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]
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.Réponses:
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 subtree
coû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:
Coût total estimé des sous-arbres pour votre plan le plus performant
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
Subtreecost
est calculée sur leindex seek
&nested loops operator
effectuant la jointure:Alors que pour notre mauvais plan de requête, le
Clustered index seek
coût de l' opérateur est inférieurCe qui devrait expliquer pourquoi l'autre plan aurait pu être choisi.
(Et en ajoutant le paramètre
30
augmentant le coût du mauvais plan là où il a dépassé le871.510000
coût estimé). Estimation approximative ™Le plan le plus performant
Le mauvais plan
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 JOIN
indice de jointureIl est plus proche, mais présente quelques différences d'ordre de jointure:
Réécriture
Ma première tentative de réécriture pourrait être de stocker tous ces nombres dans une table temporaire à la place:
Et puis ajouter un
JOIN
au lieu du grandIN()
Notre plan de requête est différent mais pas encore fixé:
avec un énorme coût d'opérateur estimé sur la
AuditRelatedIds
tableVoici 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
7497
et7498
Plus d'informations sur les filtres bitmap optimisés ici .
Cela signifie que, sans les filtres bitmap, l’optimiseur juge préférable de se joindre au
#number
table, puis de se joindre à laAuditRelatedIds
table.En forçant la commande,
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);
nous pouvons voir pourquoi:&
Pas bon
Suppression de la possibilité de passer en parallèle avec maxdop 1
Lorsque l'ajout de
MAXDOP 1
la requête s'exécute plus rapidement, à un seul thread.Et en ajoutant cet index
Lors de l'utilisation d'une jointure de fusion.
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:
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
+ lesparallellism
opé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 unmerge join
et 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
la source
MAXDOP 0
semble l'avoir corrigé. Merci beaucoup!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.ID
est 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.ID
est lié à la liste desAudit.TargetTypeID
.Depuis l'ajout de
Audit.TargetTypeID = 30
semble 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 parAuditRelatedID.RelatedID
puis trouverAudit.ID
était probablement plus sélective que d'essayer d'utiliserAudit.TargetTypeID
pour ensuite rechercherAudit.ID
.Au crédit d'Ypercube. Vous pouvez certainement mettre
[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]
à jour pour avoir à la foisRelatedID
etAuditID
dans le cadre de l'index au lieu d'avoirAuditID
dans le cadre d'unINCLUDE
. Il ne devrait pas occuper d'espace d'index supplémentaire et vous permettrait d'utiliser les deux colonnes dans lesJOIN
clauses. 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
Audit
lequel contientTargetTypeID ASC, ID ASC
sur les nœuds ordonnés / filtrage réels (pas dans le cadre deINCLUDE
). Cela devrait permettre à l'optimiseur de requête de filtrer d'ici àAudit.TargetTypeID
se 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.la source