Zusammenfassung
Die Hauptprobleme sind:
- Die Planauswahl des Optimierers setzt eine gleichmäßige Werteverteilung voraus.
- Ein Mangel an geeigneten Indizes bedeutet:
- Das Scannen der Tabelle ist die einzige Option.
- Der Join ist ein naiver Join für verschachtelte Schleifen und kein Join für verschachtelte Indexschleifen . Bei einem naiven Join werden die Join-Prädikate beim Join ausgewertet, anstatt auf die Innenseite des Joins gedrückt zu werden.
Einzelheiten
Die beiden Pläne sind grundsätzlich ziemlich ähnlich, obwohl die Leistung sehr unterschiedlich sein kann:
Planen Sie mit den zusätzlichen Spalten
Nehmen Sie zuerst die mit den zusätzlichen Spalten, die nicht in angemessener Zeit abgeschlossen sind:

Die interessanten Funktionen sind:
- Die Spitze am Knoten 0 begrenzt die zurückgegebenen Zeilen auf 100. Außerdem wird ein Zeilenziel für das Optimierungsprogramm festgelegt, sodass alles darunter im Plan ausgewählt wird, um die ersten 100 Zeilen schnell zurückzugeben.
- Der Scan am Knoten 4 findet Zeilen aus der Tabelle, in denen der Wert
Start_Timenicht null, State3 oder 4 ist und Operation_Typeeiner der aufgelisteten Werte ist. Die Tabelle wird einmal vollständig gescannt, wobei jede Zeile gegen die genannten Prädikate getestet wird. Nur Zeilen, die alle Tests bestehen, werden an die Sortierung weitergeleitet. Der Optimierer schätzt, dass 38.283 Zeilen qualifiziert sind.
- Die Sortierung an Knoten 3 belegt alle Zeilen aus dem Scan an Knoten 4 und sortiert sie in der Reihenfolge von
Start_Time DESC. Dies ist die endgültige Präsentationsreihenfolge, die von der Abfrage angefordert wird.
- Der Optimierer schätzt, dass 93 Zeilen (tatsächlich 93,2791) aus der Sortierung gelesen werden müssen, damit der gesamte Plan 100 Zeilen zurückgibt (unter Berücksichtigung des erwarteten Effekts des Joins).
- Es wird erwartet, dass der Nested Loops-Join am Knoten 2 seine innere Eingabe (den unteren Zweig) 94 Mal ausführt (tatsächlich 94.2791). Die zusätzliche Zeile wird aus technischen Gründen für den Stopp-Parallelitätsaustausch am Knoten 1 benötigt.
- Der Scan am Knoten 5 scannt die Tabelle bei jeder Iteration vollständig. Es werden Zeilen gefunden, die
Start_Timenicht null sind und State3 oder 4 betragen. Es wird geschätzt, dass bei jeder Iteration 400.875 Zeilen erzeugt werden. Über 94.2791 Iterationen beträgt die Gesamtzahl der Zeilen fast 38 Millionen.
- Der Join für verschachtelte Schleifen am Knoten 2 wendet auch die Join-Prädikate an. Es wird überprüft, ob
Operation_TypeÜbereinstimmungen vorliegen, ob der Start_Timevon Knoten 4 kleiner als der Start_Timevon Knoten 5 ist, ob der Start_Timevon Knoten 5 kleiner als der Finish_Timevon Knoten 4 ist und ob die beiden IdWerte nicht übereinstimmen.
- Die Gather Streams (Stop Parallelism Exchange) am Knoten 1 führen die geordneten Streams von jedem Thread zusammen, bis 100 Zeilen erzeugt wurden. Die auftragserhaltende Natur der Zusammenführung über mehrere Streams erfordert die in Schritt 5 erwähnte zusätzliche Zeile.
Die große Ineffizienz liegt offensichtlich in den obigen Schritten 6 und 7. Das vollständige Scannen der Tabelle am Knoten 5 für jede Iteration ist nur geringfügig sinnvoll, wenn dies nur 94 Mal geschieht, wie es der Optimierer vorhersagt. Die ~ 38 Millionen Vergleiche pro Zeile am Knoten 2 sind ebenfalls mit hohen Kosten verbunden.
Entscheidend ist auch, dass die Schätzung des Ziels der Zeilenreihe 93/94 wahrscheinlich falsch ist, da sie von der Verteilung der Werte abhängt. Der Optimierer geht von einer gleichmäßigen Verteilung aus, wenn keine detaillierteren Informationen vorliegen. In einfachen Worten bedeutet dies, dass, wenn erwartet wird, dass 1% der Zeilen in der Tabelle qualifiziert sind, der Optimierer Gründe dafür hat, dass er 100 Zeilen lesen muss, um eine übereinstimmende Zeile zu finden.
Wenn Sie diese Abfrage vollständig ausführen (was sehr lange dauern kann), werden Sie höchstwahrscheinlich feststellen, dass viel mehr als 93/94 Zeilen aus der Sortierung gelesen werden mussten, um schließlich 100 Zeilen zu erstellen. Im schlimmsten Fall wird die 100. Zeile anhand der letzten Zeile aus der Sortierung gefunden. Unter der Annahme, dass die Schätzung des Optimierers auf Knoten 4 korrekt ist, bedeutet dies, dass der Scan auf Knoten 5 38.284 Mal ausgeführt wird, was insgesamt etwa 15 Milliarden Zeilen entspricht. Es könnte mehr sein, wenn die Scan-Schätzungen ebenfalls deaktiviert sind.
Dieser Ausführungsplan enthält auch eine fehlende Indexwarnung:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
Das Optimierungsprogramm weist Sie darauf hin, dass das Hinzufügen eines Index zur Tabelle die Leistung verbessern würde.
Planen Sie ohne die zusätzlichen Spalten

Dies ist im Wesentlichen derselbe Plan wie der vorherige, mit der Hinzufügung der Indexspule an Knoten 6 und des Filters an Knoten 5. Die wichtigen Unterschiede sind:
- Die Indexspule am Knoten 6 ist eine eifrige Spule. Er verbraucht mit Spannung das Ergebnis der Scan darunter, und baut einen temporären Index verkeilt
Operation_Typeund Start_Timemit Idals Nicht-Schlüsselspalte.
- Der Nested Loops Join am Knoten 2 ist jetzt ein Index Join. Keine Joinvergleichselemente werden hier ausgewertet, sondern die per-Iteration aktuellen Werte von
Operation_Type, Start_Time, Finish_Time, und Idaus der Abtastung an Knoten 4 weitergeleitet werden an den Innenseiten - Zweig als äußere Referenzen.
- Der Scan am Knoten 7 wird nur einmal durchgeführt.
- Der Index Spool am Knoten 6 sucht Zeilen aus dem temporären Index wo
Operation_Typeentspricht den aktuellen äußeren Referenzwert ist , und das Start_Timewird in dem durch die definierten Bereich Start_Timeund Finish_Timeäußern Referenzen.
- Der Filter am Knoten 5 testet
IdWerte aus der Indexspule auf Ungleichheit mit dem aktuellen äußeren Referenzwert von Id.
Die wichtigsten Verbesserungen sind:
- Der Innenseiten-Scan wird nur einmal durchgeführt
- Ein temporärer Index für (
Operation_Type, Start_Time) mit Idals eingeschlossene Spalte ermöglicht die Verknüpfung eines Index verschachtelter Schleifen. Der Index wird verwendet, um bei jeder Iteration nach übereinstimmenden Zeilen zu suchen, anstatt jedes Mal die gesamte Tabelle zu scannen.
Nach wie vor enthält das Optimierungsprogramm eine Warnung vor einem fehlenden Index:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
Fazit
Der Plan ohne die zusätzlichen Spalten ist schneller, da der Optimierer einen temporären Index für Sie erstellt hat.
Der Plan mit den zusätzlichen Spalten würde die Erstellung des temporären Index verteuern. Die [ParametersSpalte] gibt nvarchar(2000)bis zu 4000 Byte für jede Zeile des Index an. Die zusätzlichen Kosten reichen aus, um den Optimierer davon zu überzeugen, dass sich das Erstellen des temporären Index für jede Ausführung nicht auszahlt.
Der Optimierer warnt in beiden Fällen, dass ein permanenter Index eine bessere Lösung wäre. Die ideale Zusammensetzung des Index hängt von Ihrer Arbeitsbelastung ab. Für diese spezielle Abfrage sind die vorgeschlagenen Indizes ein vernünftiger Ausgangspunkt, aber Sie sollten die damit verbundenen Vorteile und Kosten verstehen.
Empfehlung
Eine breite Palette möglicher Indizes wäre für diese Abfrage von Vorteil. Der wichtige Aspekt ist, dass eine Art nicht gruppierter Index benötigt wird. Aus den bereitgestellten Informationen wäre meiner Meinung nach ein angemessener Index:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Ich wäre auch versucht, die Abfrage etwas besser zu organisieren und das Nachschlagen der breiten [Parameters]Spalten im Clustered-Index zu verzögern, bis die 100 besten Zeilen gefunden wurden ( Idals Schlüssel):
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Wenn die [Parameters]Spalten nicht benötigt werden, kann die Abfrage vereinfacht werden, um:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Der FORCESEEKHinweis soll sicherstellen, dass das Optimierungsprogramm einen Plan für indizierte verschachtelte Schleifen auswählt (es besteht eine kostenbasierte Versuchung für das Optimierungsprogramm, ansonsten einen Hash oder (viele, viele) Zusammenführungsverknüpfungen auszuwählen, was bei dieser Art von nicht gut funktioniert Abfrage in der Praxis. Beide haben große Residuen (viele Elemente pro Bucket im Fall des Hash und viele Rückspulen für die Zusammenführung).
Alternative
Wenn die Abfrage (einschließlich ihrer spezifischen Werte) für die Leseleistung besonders kritisch wäre, würde ich stattdessen zwei gefilterte Indizes in Betracht ziehen:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
Für die Abfrage, für die die [Parameters]Spalte nicht benötigt wird, lautet der geschätzte Plan unter Verwendung der gefilterten Indizes:

Der Index-Scan gibt automatisch alle qualifizierenden Zeilen zurück, ohne zusätzliche Prädikate auszuwerten. Für jede Iteration des Joins mit verschachtelten Indexschleifen führt die Indexsuche zwei Suchoperationen aus:
- Ein Such Prefix - Match auf
Operation_Typeund State= 3, dann den Bereich der Suche nach Start_TimeWerten, Rest Prädikat auf der IdUngleichheit.
- Eine Suchpräfixübereinstimmung mit
Operation_Typeund State= 4, dann Suche nach dem Wertebereich Start_Time, verbleibendes Prädikat für die IdUngleichung.
Wenn die [Parameters]Spalte benötigt wird, fügt der Abfrageplan einfach maximal 100 Singleton-Lookups für jede Tabelle hinzu:

Abschließend sollten Sie in Betracht ziehen, die integrierten Standard-Integer-Typen anstelle der numericggf. zu verwenden.