Requêtes très similaires, performances très différentes

9

J'ai deux requêtes très similaires

Première requête:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Résultat: 267479

Plan: https://www.brentozar.com/pastetheplan/?id=BJWTtILyS


Deuxième requête:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Résultat: 25650

Plan: https://www.brentozar.com/pastetheplan/?id=S1v79U8kS


La première requête prend environ une seconde pour se terminer, tandis que la deuxième requête prend environ 20 secondes. C'est complètement contre-intuitif pour moi car la première requête a un nombre beaucoup plus élevé que la seconde. C'est sur SQL Server 2012

Pourquoi y a-t-il tant de différence? Comment accélérer la deuxième requête pour qu'elle soit aussi rapide que la première?


Voici le script de création de table pour les deux tables:

CREATE TABLE [dbo].[AuditRelatedIds](
    [AuditId] [bigint] NOT NULL,
    [RelatedId] [uniqueidentifier] NOT NULL,
    [AuditTargetTypeId] [smallint] NOT NULL,
 CONSTRAINT [PK_AuditRelatedIds] PRIMARY KEY CLUSTERED 
(
    [AuditId] ASC,
    [RelatedId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_INCLUDES] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC
)
INCLUDE (   [AuditId]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id] FOREIGN KEY([AuditId])
REFERENCES [dbo].[Audits] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([AuditTargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id]

CREATE TABLE [dbo].[Audits](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [TargetTypeId] [smallint] NOT NULL,
    [TargetId] [nvarchar](40) NOT NULL,
    [TargetName] [nvarchar](max) NOT NULL,
    [Action] [tinyint] NOT NULL,
    [ActionOverride] [tinyint] NULL,
    [Date] [datetime] NOT NULL,
    [UserDisplayName] [nvarchar](max) NOT NULL,
    [DescriptionData] [nvarchar](max) NULL,
    [IsNotification] [bit] NOT NULL,
 CONSTRAINT [PK_Audits] 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] TEXTIMAGE_ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetId] ON [dbo].[Audits]
(
    [TargetId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetTypeIdAction_INCLUDES] ON [dbo].[Audits]
(
    [TargetTypeId] ASC,
    [Action] ASC
)
INCLUDE (   [TargetId],
    [UserDisplayName]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY]

ALTER TABLE [dbo].[Audits]  WITH CHECK ADD  CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([TargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[Audits] CHECK CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id]
Chocoman
la source
3
Serions-nous en mesure d'obtenir un schéma de table et des détails d'index. Comme je suis sûr que vous avez remarqué que les plans sont un peu différents, mais cela fait évidemment une grande différence. Si nous pouvons obtenir ces détails, nous pourrons peut-être voir quelles options nous avons.
Kirk Saunders
2
Comme astuce très rapide, au lieu d'utiliser IN, créez un TempTable avec une seule colonne TINYINT / INT (en cluster) avec les nombres souhaités, puis INNER JOIN. En dehors de cela, nous aurons probablement besoin d'informations DDL comme @KirkSaunders mentionné ci-dessus
George.Palacios
2
Y a-t-il quelque chose de spécial TargetTypeId = 30? Il semble que les plans soient différents, car cette seule valeur fausse vraiment la quantité de données (qui devraient être) renvoyées.
Aaron Bertrand
Je me rends compte que c'est terriblement pédant mais la déclaration "la première requête renvoie beaucoup plus de lignes que la seconde". n'est pas correcte. Les deux retournent 1 ligne;)
ypercubeᵀᴹ
1
J'ai mis à jour la question avec les instructions create table pour les deux tables
Chocoman

Réponses:

8

Tl; dr en bas

Pourquoi le mauvais plan a-t-il été choisi

La principale raison de choisir un plan plutôt que l'autre est le Estimated total subtreecoût.

Ce coût était plus faible pour le mauvais plan que pour le plan plus performant.

Coût total estimé des sous-arbres pour le mauvais plan:

entrez la description de l'image ici

Coût total estimé des sous-arbres pour votre plan le plus performant

entrez la description de l'image ici


L'opérateur a estimé les coûts

Certains opérateurs peuvent assumer la majeure partie de ce coût et pourraient être une raison pour l'optimiseur de choisir un chemin / plan différent.

Dans notre plan plus performant, la majeure partie de Subtreecostest calculée sur le index seek& nested loops operatoreffectuant la jointure:

entrez la description de l'image ici

Alors que pour notre mauvais plan de requête, le Clustered index seekcoût de l' opérateur est inférieur

entrez la description de l'image ici

Ce qui devrait expliquer pourquoi l'autre plan aurait pu être choisi.

(Et en ajoutant le paramètre 30augmentant le coût du mauvais plan là où il a dépassé le 871.510000coût estimé). Estimation approximative ™

Le plan le plus performant

entrez la description de l'image ici

Le mauvais plan

entrez la description de l'image ici


Où cela nous mène-t-il?

Ces informations nous amènent à un moyen de forcer le mauvais plan de requête sur notre exemple (voir DML pour presque répliquer le problème d'OP pour les données utilisées pour répliquer le problème)

En ajoutant un INNER LOOP JOINindice de jointure

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Il est plus proche, mais présente quelques différences d'ordre de jointure:

entrez la description de l'image ici


Réécriture

Ma première tentative de réécriture pourrait être de stocker tous ces nombres dans une table temporaire à la place:

CREATE TABLE #Numbers(Numbering INT)
INSERT INTO #Numbers(Numbering)
VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(11),(12),(13),(14),(15),(16),(17),(18),(19),
(21),(22),(23),(24),(25),(26),(27),(28),(29),(30),(31),(32),(33),(34),(35),
(36),(37),(38),(39),(41),(42),(43),(44),(45),(46),(47),(48),(49),(51),(52),
(53),(54),(55),(56),(57),(58),(59),(61),(62),(63),(64),(65),(66),(67),(68),
(69),(71),(72),(73),(74),(75),(76),(77),(78),(79);

Et puis ajouter un JOINau lieu du grandIN()

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1';

Notre plan de requête est différent mais pas encore fixé:

entrez la description de l'image ici

avec un énorme coût d'opérateur estimé sur la AuditRelatedIdstable

entrez la description de l'image ici


Voici où j'ai remarqué que

La raison pour laquelle je ne peux pas recréer directement votre plan est le filtrage bitmap optimisé.

Je peux recréer votre plan en désactivant les filtres bitmap optimisés à l'aide de traceflags 7497et7498

SELECT count(*)
FROM Audits a 
   INNER JOIN AuditRelatedIds  ari ON a.Id = ari.AuditId 
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498);

Plus d'informations sur les filtres bitmap optimisés ici .

entrez la description de l'image ici

Cela signifie que, sans les filtres bitmap, l’optimiseur juge préférable de se joindre au #number table, puis de se joindre à la AuditRelatedIdstable.

En forçant la commande, OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);nous pouvons voir pourquoi:

entrez la description de l'image ici

& entrez la description de l'image ici

Pas bon


Suppression de la possibilité de passer en parallèle avec maxdop 1

Lorsque l'ajout de MAXDOP 1la requête s'exécute plus rapidement, à un seul thread.

Et en ajoutant cet index

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_AuditId] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC,
    [AuditId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];

entrez la description de l'image ici

Lors de l'utilisation d'une jointure de fusion. entrez la description de l'image ici

La même chose est vraie lorsque nous supprimons l'indicateur de requête d'ordre de force ou non en utilisant la table #Numbers et en utilisant le IN() place.

Mon conseil serait de chercher à ajouter MAXDOP(1) et de voir si cela aide votre requête, avec une réécriture si nécessaire.

Bien sûr, vous devez également garder à l'esprit que, de mon côté, il fonctionne encore mieux en raison du filtrage bitmap optimisé et de l'utilisation effective de plusieurs threads pour un bon effet:

entrez la description de l'image ici

entrez la description de l'image ici


TL; DR

Les coûts estimés définiront le plan choisi, j'ai pu reproduire le comportement et j'ai vu que optimized bitmap filters + les parallellismopérateurs ajoutés de mon côté pour effectuer la requête de manière performante et rapide.

Vous pouvez envisager d'ajouter MAXDOP(1)à votre requête un moyen, espérons-le, d'obtenir le même résultat contrôlé à chaque fois, avec un merge joinet aucun `` mauvais ''parallellism .

Mise à niveau vers une version plus récente et utilisation d'une version d'estimateur de cardinalité plus élevée que CardinalityEstimationModelVersion="70" pourrait également aider.

Une table temporaire de nombres pour effectuer le filtrage multi-valeurs peut également aider.


DML va presque répliquer le problème de l'OP

J'ai passé plus de temps là-dessus que je ne voudrais l'admettre

set NOCOUNT ON;
DECLARE @I INT = 0
WHILE @I < 56
BEGIN
INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(500000) CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 END as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;
SET @I +=1;
END

-- 'Bad Query matches'
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])
SELECT
TOP(25650)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') , 
CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 END as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2

-- Extra matches with 30
SELECT MAX([Id]) FROM [dbo].[Audits];
--28000001 Upper value

INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(241829) 30 as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;



;WITH CTE AS
(SELECT
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') as gu , 
30 as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2
CROSS APPLY master.dbo.spt_values spt3
)
--267479 - 25650 = 241829
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])

SELECT TOP(241829) rownum1,gu,rownum2 FROM CTE
WHERE rownum1 > 28000001
ORDER BY rownum1 ASC;
Randi Vertongen
la source
Très belle explication! L'ajout MAXDOP 0semble l'avoir corrigé. Merci beaucoup!
Chocoman
1
MAXDOP 1 ** (faute de frappe)
Chocoman
@Chocoman Great! Heureux d'aider :)
Randi Vertongen
1

D'après ce que je peux dire, la principale différence entre les deux plans est la différence dans ce qui est le "filtre principal".

Avec la première version, le filtre principal dérivait qui Audit.IDest lié à ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'ensuite filtrer cette liste vers le bas pour ceux qui sontAudit.TargetTypeID étaient dans la liste.

Avec la deuxième version dérivait le filtre principal qui Audit.IDest lié à la liste des Audit.TargetTypeID.

Depuis l'ajout de Audit.TargetTypeID = 30semble augmenter considérablement le nombre d'enregistrements (267 479 et 25 650 respectivement selon la question d'origine). C'est probablement pourquoi les plans d'exécution sont différents. (Si je comprends bien) SQL essaiera d'abord de faire la fonction la plus sélective, puis appliquera le reste des règles par la suite. Avec la première version, l'interrogation par AuditRelatedID.RelatedIDpuis trouver Audit.IDétait probablement plus sélective que d'essayer d'utiliser Audit.TargetTypeIDpour ensuite rechercher Audit.ID.

Au crédit d'Ypercube. Vous pouvez certainement mettre [AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]à jour pour avoir à la fois RelatedIDet AuditIDdans le cadre de l'index au lieu d'avoir AuditIDdans le cadre d'un INCLUDE. Il ne devrait pas occuper d'espace d'index supplémentaire et vous permettrait d'utiliser les deux colonnes dans les JOINclauses. Cela peut aider l'Optimiseur de requête à créer le même plan d'exécution pour les deux requêtes.

Fonctionnant avec une logique similaire, il peut y avoir un certain avantage à un index sur Auditlequel contient TargetTypeID ASC, ID ASCsur les nœuds ordonnés / filtrage réels (pas dans le cadre de INCLUDE). Cela devrait permettre à l'optimiseur de requête de filtrer d'ici à Audit.TargetTypeIDse joindre rapidement à AuditReferenceIds.AuditID. Maintenant, cela peut se retrouver avec les deux requêtes choisissant le plan le moins efficace, donc je ne lui donnerai un coup de feu qu'après avoir essayé la recommandation de ypercube.

Kirk Saunders
la source