Sehr ähnliche Abfragen, sehr unterschiedliche Leistung

9

Ich habe zwei sehr ähnliche Fragen

Erste Abfrage:

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)

Ergebnis: 267479

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


Zweite Abfrage:

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)

Ergebnis: 25650

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


Die erste Abfrage dauert ungefähr eine Sekunde, während die zweite Abfrage ungefähr 20 Sekunden dauert. Dies ist für mich völlig kontraintuitiv, da die erste Abfrage eine viel höhere Anzahl als die zweite hat. Dies ist auf SQL Server 2012

Warum gibt es so einen großen Unterschied? Wie kann ich die zweite Abfrage so beschleunigen, dass sie so schnell ist wie die erste?


Hier ist das Skript zum Erstellen einer Tabelle für beide Tabellen:

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
quelle
3
Würden wir in der Lage sein, einige Tabellenschema- und Indexdetails zu erhalten? Wie Sie sicher bemerkt haben, sind die Pläne etwas anders, aber offensichtlich macht es einen großen Unterschied. Wenn wir diese Details erhalten können, können wir vielleicht sehen, welche Optionen wir haben.
Kirk Saunders
2
Als sehr schnellen Tipp können Sie anstelle von IN eine TempTable mit einer einzelnen TINYINT / INT-Spalte (gruppiert) mit den gewünschten Zahlen erstellen und dann INNER JOIN hinzufügen. Ansonsten benötigen wir wahrscheinlich DDL-Informationen wie oben erwähnt
@KirkSaunders
2
Gibt es etwas Besonderes TargetTypeId = 30? Die Pläne scheinen unterschiedlich zu sein, da dieser eine Wert die (voraussichtlich) zurückgegebene Datenmenge wirklich verzerrt.
Aaron Bertrand
Mir ist klar, dass es furchtbar pedantisch ist, aber die Aussage "Die erste Abfrage gibt viel mehr Zeilen zurück als die zweite." das ist nicht richtig. Beide geben 1 Zeile zurück;)
ypercubeᵀᴹ
1
Ich habe die Frage mit den Anweisungen zum Erstellen von Tabellen für beide Tabellen aktualisiert
Chocoman

Antworten:

8

Tl; dr unten

Warum wurde der schlechte Plan gewählt?

Der Hauptgrund für die Wahl eines Plans gegenüber dem anderen sind die Estimated total subtreeKosten.

Diese Kosten waren für den schlechten Plan niedriger als für den Plan mit der besseren Leistung.

Die geschätzten Gesamtkosten des Teilbaums für den schlechten Plan:

Geben Sie hier die Bildbeschreibung ein

Die geschätzten Gesamtkosten für den Teilbaum für Ihren Plan mit besserer Leistung

Geben Sie hier die Bildbeschreibung ein


Der Betreiber schätzte die Kosten

Bestimmte Betreiber können den größten Teil dieser Kosten tragen und könnten ein Grund für den Optimierer sein, einen anderen Pfad / Plan zu wählen.

In unserem Plan mit besserer Leistung wird der Großteil der Subtreecostauf dem index seek& durchgeführten nested loops operatorJoin berechnet :

Geben Sie hier die Bildbeschreibung ein

Während für unseren schlechten Abfrageplan die Clustered index seekBetreiberkosten niedriger sind

Geben Sie hier die Bildbeschreibung ein

Das sollte erklären, warum der andere Plan hätte gewählt werden können.

(Und durch Hinzufügen des Parameters 30, der die Kosten des fehlerhaften Plans erhöht, wenn er über die 871.510000geschätzten Kosten gestiegen ist ). Geschätzte Vermutung ™

Der leistungsfähigere Plan

Geben Sie hier die Bildbeschreibung ein

Der schlechte Plan

Geben Sie hier die Bildbeschreibung ein


Wohin führt uns das?

Diese Informationen bringen uns zu einer Möglichkeit, den Plan für fehlerhafte Abfragen in unserem Beispiel zu erzwingen (siehe DML, um das Problem von OP für die Daten, die zum Replizieren des Problems verwendet wurden, fast zu replizieren).

Durch Hinzufügen eines INNER LOOP JOINJoin-Hinweises

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)

Es ist näher, weist jedoch einige Unterschiede in der Reihenfolge der Verknüpfungen auf:

Geben Sie hier die Bildbeschreibung ein


Umschreiben

Mein erster Umschreibversuch könnte darin bestehen, alle diese Zahlen stattdessen in einer temporären Tabelle zu speichern:

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);

Und dann ein JOINanstelle des großen hinzufügenIN()

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';

Unser Abfrageplan ist anders, aber noch nicht festgelegt:

Geben Sie hier die Bildbeschreibung ein

mit riesigen geschätzten Betreiberkosten auf dem AuditRelatedIdsTisch

Geben Sie hier die Bildbeschreibung ein


Hier ist mir das aufgefallen

Der Grund, warum ich Ihren Plan nicht direkt neu erstellen kann, ist die optimierte Bitmap-Filterung.

Ich kann Ihren Plan neu erstellen, indem ich optimierte Bitmap-Filter mithilfe von Traceflags 7497& deaktiviere7498

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);

Weitere Informationen zu optimierten Bitmap-Filtern finden Sie hier .

Geben Sie hier die Bildbeschreibung ein

Dies bedeutet, dass es für den Optimierer ohne die Bitmap-Filter besser ist, zuerst mit der #numberTabelle und dann mit der AuditRelatedIdsTabelle zu verbinden.

Wenn OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);wir die Bestellung erzwingen, können wir sehen warum:

Geben Sie hier die Bildbeschreibung ein

& Geben Sie hier die Bildbeschreibung ein

Nicht gut


Entfernen der Fähigkeit, parallel zu maxdop 1 zu gehen

Beim Hinzufügen führt MAXDOP 1die Abfrage eine schnellere Single-Thread-Ausführung durch.

Und diesen Index hinzufügen

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];

Geben Sie hier die Bildbeschreibung ein

Bei Verwendung eines Merge-Joins. Geben Sie hier die Bildbeschreibung ein

Das Gleiche gilt, wenn wir den Abfragehinweis für die erzwungene Reihenfolge entfernen oder die Tabelle #Numbers nicht verwenden und IN()stattdessen die Tabelle verwenden .

Mein Rat wäre, das Hinzufügen zu prüfen MAXDOP(1)und zu prüfen, ob dies Ihrer Anfrage hilft, und bei Bedarf eine Neufassung vorzunehmen.

Natürlich sollten Sie auch bedenken, dass es meiner Meinung nach aufgrund der optimierten Bitmap-Filterung und der tatsächlichen Verwendung mehrerer Threads noch besser funktioniert:

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein


TL; DR

Die geschätzten Kosten definieren den gewählten Plan. Ich konnte das Verhalten replizieren und sah, dass optimized bitmap filters+ parallellismOperatoren an meinem Ende hinzugefügt wurden, um die Abfrage auf performante und schnelle Weise durchzuführen.

Sie könnten versuchen MAXDOP(1), Ihre Abfrage zu erweitern, um hoffentlich jedes Mal das gleiche kontrollierte Ergebnis zu erzielen, mit einem merge joinund ohne "schlecht" parallellism.

Ein Upgrade auf eine neuere Version und die Verwendung einer Version mit einem höheren Kardinalitätsschätzer als dies CardinalityEstimationModelVersion="70"möglicherweise ebenfalls hilfreich ist.

Eine temporäre Zahlentabelle für die Mehrwertfilterung kann ebenfalls hilfreich sein.


DML, um das Problem von OP fast zu replizieren

Ich habe mehr Zeit damit verbracht, als ich zugeben möchte

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
quelle
Sehr schöne Erklärung! Das Hinzufügen MAXDOP 0scheint es behoben zu haben. Vielen Dank!
Chocoman
1
MAXDOP 1 ** (Tippfehler)
Chocoman
@Chocoman Großartig! Gerne helfen :)
Randi Vertongen
1

Nach allem, was ich sagen kann, ist der Hauptunterschied zwischen den beiden Plänen der Unterschied im "Primärfilter".

Bei der ersten Version wurde der Hauptfilter abgeleitet, der Audit.IDsich darauf bezieht, ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'diese Liste auf diejenigen zu filtern, Audit.TargetTypeIDdie in der Liste enthalten waren.

Mit der zweiten Version wurde der Hauptfilter abgeleitet, der Audit.IDsich auf die Liste von bezieht Audit.TargetTypeID.

Seit der Hinzufügung von Audit.TargetTypeID = 30schien sich die Rekordzahl dramatisch zu erhöhen (267.479 bzw. 25.650 gemäß der ursprünglichen Frage). Das ist wahrscheinlich der Grund, warum die Ausführungspläne unterschiedlich sind. (So ​​wie ich es verstehe) SQL wird versuchen, zuerst die selektivste Funktion auszuführen und danach den Rest der Regeln anzuwenden. Bei der ersten Version war das Abfragen bis AuditRelatedID.RelatedIDzum Finden Audit.IDwahrscheinlich selektiver als der Versuch, das Audit.TargetTypeIDFinden zu verwenden Audit.ID.

Zu Ypercubes Gunsten. Sie können sicher aktualisieren [AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES], um beide RelatedIDund AuditIDals Teil des Index zu haben, anstatt AuditIDals Teil eines INCLUDE. Es sollte keinen zusätzlichen Indexplatz beanspruchen und es Ihnen ermöglichen, beide Spalten in JOINKlauseln zu verwenden. Dies kann dazu beitragen, dass das Abfrageoptimierungsprogramm für beide Abfragen denselben Ausführungsplan erstellt.

Wenn Sie mit einer ähnlichen Logik arbeiten, kann ein Index, Auditder TargetTypeID ASC, ID ASCdie tatsächlich geordneten / Filterknoten enthält (nicht als Teil von INCLUDE) , einige Vorteile haben . Dies sollte es dem Abfrageoptimierer ermöglichen, zu filtern und Audit.TargetTypeIDdann schnell zu verbinden AuditReferenceIds.AuditID. Dies kann dazu führen, dass beide Abfragen den weniger effizienten Plan auswählen, sodass ich ihn erst ausprobieren würde, nachdem ich die Empfehlung von ypercube ausprobiert habe.

Kirk Saunders
quelle