Données intrinsèquement ordonnées comme s'il s'agissait d'un index clusterisé


8

J'ai le tableau suivant avec 7,5 millions d'enregistrements:

CREATE TABLE [dbo].[TestTable](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [TestCol] [nvarchar](50) NOT NULL,
    [TestCol2] [nvarchar](50) NOT NULL,
    [TestCol3] [nvarchar](50) NOT NULL,
    [Anonymised] [tinyint] NOT NULL,
    [Date] [datetime] NOT NULL,
CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Je remarque que lorsqu'il y a un index non clusterisé sur le champ de date:

CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date])

-et j'exécute la requête suivante:

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Date] <= '25 August 2016'

- les données provenant de l'opération d'accès à l'index sont triées pour correspondre à l'ordre des clés du PK / CX, ce qui réduit les performances.

Plan de requête

J'ai été surpris de constater que la suppression de l'index du champ date améliore réellement les performances de la requête d'environ 30% car il n'effectue plus le tri:

Plan de requête

Ma théorie, et cela peut être évident pour les plus expérimentés d'entre vous, est qu'elle a compris que la colonne de date est implicitement ordonnée exactement de la même manière que la clé primaire / l'index clusterisé.

Ma question est donc: est-il possible de profiter de ce fait pour améliorer les performances de ma requête?


1
Je n'ai pas regardé les plans mais je soupçonne que les performances (enfin, la durée, aucun de ces chiffres de coût estimatif inutiles) se sont améliorées car il n'a plus à mettre à jour l'index que vous avez supprimé, pas à cause d'une opération de tri.
Aaron Bertrand

@AaronBertrand Je les lis peut-être incorrectement, veuillez donc me corriger si je me trompe, mais il semble y avoir une opération de mise à jour d'index dans les deux plans de requête. Parlez-vous d'autre chose?
AproposArmadillo

1
Encore une fois, j'ai dit, je n'ai pas regardé les plans. Vous avez dit que "la suppression de l'index du champ de date améliore les performances de la requête" ... si vous supprimez l'index, il ne devrait pas apparaître dans le plan, alors peut-être avez-vous collecté le mauvais plan ou n'avez pas réellement supprimé le index que vous pensiez avoir fait. Et encore une fois, certains% estimés pour un plan sont un indicateur mais ne reflètent en aucune façon la véritable mesure du rendement. Il s'agit d'une estimation qui est calculée avant même l'exécution de la requête.
Aaron Bertrand

@Aaron Bertrand, il n'a de toute façon pas dû mettre à jour l'index, car [Date] n'était pas parmi les champs mis à jour.
Denis Rubashkin

1
@Shaffanhoon Avez-vous essayé de recréer l'index [Date]mais dans l' DESCordre? Juste curieux puisque le prédicat l'est <=. De plus, si l'index on Date(par défaut, ACSorder) aide d'autres requêtes, alors vous pouvez peut-être essayer d'ajouter un indice de table à UPDATE pour le forcer à utiliser le PK? Ou, peut-être diviser cela en deux parties: créer une table temporaire, remplir avec [Id]basé sur [Date] <= '25 August 2016', puis supprimer le WHEREde la MISE À JOUR et l'ajouter FROM dbo.TestTable tt INNER JOIN #tmp ids ON ids.[Id] = tt.[Id]. C'est une MISE À JOUR après tout, et il doit trouver les lignes réelles, l'index ou non.
Solomon Rutzky

Réponses:


7

J'ai simulé des données de test qui reproduisent principalement votre problème:

INSERT INTO [dbo].[TestTable] WITH (TABLOCK)
SELECT TOP (7000000) N'*NOT GDPR*', N'*NOT GDPR*', N'*NOT GDPR*', 0, DATEADD(DAY, q.RN  / 16965, '20160801')
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
ORDER BY q.RN
OPTION (MAXDOP 1);


DROP INDEX IF EXISTS [dbo].[TestTable].IX_TestTable_Date;
CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date]);

Statistiques pour la requête qui utilise l'index non cluster:

Tableau «TestTable». Nombre de balayages 1, lectures logiques 1299838, lectures physiques 0, lectures en lecture anticipée 0, lectures logiques en lob 0, lectures physiques en lob 0, lobes en lecture anticipée 0.

Temps d'exécution SQL Server: temps CPU = 984 ms, temps écoulé = 988 ms.

Statistiques pour la requête qui utilise l'index cluster:

Tableau «TestTable». Nombre de balayages 1, lectures logiques 72609, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

Temps d'exécution SQL Server: temps CPU = 781 ms, temps écoulé = 772 ms.

Accéder à votre question:

Est-il possible de profiter de ce fait pour améliorer les performances de ma requête?

Oui. Vous pouvez utiliser l'index non cluster dont vous disposez déjà pour trouver efficacement la idvaleur maximale à mettre à jour. Si vous enregistrez cela dans une variable et que vous le filtrez, vous obtenez un plan de requête pour la mise à jour qui effectue l'analyse d'index en cluster (sans le tri) qui s'arrête tôt et fait donc moins d'E / S. Voici une implémentation:

DECLARE @Id INT;

SELECT TOP (1) @Id = Id
FROM dbo.TestTable 
WHERE [Date] <= '25 August 2016'
ORDER BY [Date] DESC, Id DESC;

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Id] < @Id AND [Date] <= '25 August 2016'
AND [Anonymised] <> 1 -- optional
OPTION (MAXDOP 1);

Exécutez les statistiques pour la nouvelle requête:

Tableau «TestTable». Nombre de balayages 1, lectures logiques 3, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

Tableau «TestTable». Nombre de balayages 1, lectures logiques 4776, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

Temps d'exécution SQL Server: temps CPU = 515 ms, temps écoulé = 510 ms.

Ainsi que le plan de requête:

plan de requête ok

Cela dit, votre désir d'accélérer la requête me suggère que vous prévoyez d'exécuter la requête plus d'une fois. À l'heure actuelle, votre requête a un filtre à composition non limitée sur la datecolonne. Est-il vraiment nécessaire d'anonymiser les lignes plus d'une fois? Pouvez-vous éviter de mettre à jour ou d'analyser des lignes déjà anonymisées? Il devrait certainement être plus rapide de mettre à jour une plage de dates avec des dates des deux côtés. Vous pouvez également ajouter la Anonymisedcolonne à votre index, mais cet index devra être mis à jour lors de votre UPDATErequête. En résumé, évitez de traiter les mêmes données encore et encore si vous le pouvez.

La requête d'origine que vous avez avec le tri est plus lente en raison du travail effectué dans l' Clustered Index Updateopérateur. Le temps passé sur la recherche d'index et le tri n'est que de 407 ms. Vous pouvez le voir dans le plan réel. Le plan s'exécute en mode ligne, le temps passé sur le tri est donc le temps de cet opérateur avec chaque opérateur enfant:

entrez la description de l'image ici

Cela laisse l'opérateur de tri avec environ 1600 ms de temps. SQL Server doit lire les pages de l'index cluster pour effectuer la mise à jour. Vous pouvez voir que l' Clustered Index Updateopérateur effectue des lectures logiques 1205921. Vous pouvez en savoir plus sur les optimisations de tri pour DML et la prélecture optimisée dans cet article de blog de Paul White .

L'autre plan de requête que vous avez (sans le tri) prend 683 ms pour l'analyse d'index cluster et environ 550 ms pour l' Clustered Index Updateopérateur. L'opérateur de mise à jour ne fait pas d'E / S pour cette requête.

La réponse simple pour expliquer pourquoi le plan avec le tri est plus lent est que SQL Server effectue des lectures plus logiques sur l'index cluster pour ce plan par rapport au plan d'analyse d'index cluster. Même si toutes les données nécessaires sont en mémoire, il y a toujours un surcoût et un coût pour effectuer ces lectures logiques. Une meilleure réponse est beaucoup plus difficile à obtenir, dans la mesure où je sais que les plans ne vous donneront pas plus de détails. Il est possible d'utiliser PerfView ou un autre outil basé sur le traçage ETW pour comparer les piles d'appels entre les requêtes:

entrez la description de l'image ici

À gauche se trouve la requête qui effectue l'analyse d'index en cluster et à droite se trouve la requête qui effectue le tri. J'ai marqué les piles d'appels en bleu ou en rouge qui n'apparaissent que dans une seule requête. Sans surprise, les différentes piles d'appels avec un nombre élevé de cycles CPU échantillonnés pour la requête de tri semblent avoir à voir avec les lectures logiques requises pour effectuer la mise à jour sur l'index clusterisé. De plus, il existe des différences dans le nombre de cycles échantillonnés entre les requêtes pour la même opération. Par exemple, la requête avec le tri passe 31 cycles à acquérir des verrous tandis que la requête avec l'analyse ne passe que 9 cycles à acquérir des verrous.

Je soupçonne que SQL Server choisit le plan le plus lent en raison d'une limitation des coûts de l'opérateur du plan de requête. Peut-être qu'une partie de la différence de temps d'exécution est due au matériel ou à votre édition de SQL Server. Dans tous les cas, SQL Server n'est pas en mesure de comprendre que la colonne de date est implicitement ordonnée exactement de la même manière que l'index cluster. Les données sont renvoyées à partir de l'analyse d'index en cluster dans l'ordre des clés en cluster, il n'est donc pas nécessaire d'effectuer un tri pour tenter d'optimiser les E / S lors de la mise à jour de l'index en cluster.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.