Es scheint drei verschiedene Optimierungsregeln zu geben, die den DISTINCT
Vorgang in der obigen Abfrage ausführen können . Die folgende Abfrage löst einen Fehler aus, der darauf hindeutet, dass die Liste vollständig ist:
SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);
Nachricht 8622, Ebene 16, Status 1, Zeile 1
Der Abfrageprozessor konnte aufgrund der in dieser Abfrage definierten Hinweise keinen Abfrageplan erstellen. Senden Sie die Abfrage erneut, ohne Hinweise anzugeben und ohne SET FORCEPLAN zu verwenden.
GbAggToSort
Implementiert das Group-By-Aggregat (distinct) als verschiedene Sortierung. Dies ist ein Blockierungsoperator, der alle Daten aus der Eingabe liest, bevor Zeilen erstellt werden. GbAggToStrm
Implementiert das Group-by-Aggregat als Stream-Aggregat (was in dieser Instanz auch eine Eingabesortierung erfordert). Dies ist auch ein Blockierungsoperator. GbAggToHS
implementiert als Hash-Match, was wir in dem schlechten Plan aus der Frage gesehen haben, aber es kann als Hash-Match (aggregiert) oder Hash-Match (flow distinct) implementiert werden.
Der Hash-Match- Operator ( flow distinct ) ist eine Möglichkeit, dieses Problem zu lösen, da er nicht blockiert. SQL Server sollte in der Lage sein, den Scan zu stoppen, sobald genügend unterschiedliche Werte gefunden wurden.
Der logische Operator "Flow Distinct" durchsucht die Eingabe und entfernt Duplikate. Während der Operator "Distinct" alle Eingaben verbraucht, bevor eine Ausgabe erstellt wird, gibt der Operator "Flow Distinct" jede Zeile so zurück, wie sie aus der Eingabe abgerufen wird (es sei denn, diese Zeile ist ein Duplikat. In diesem Fall wird sie verworfen).
Warum verwendet die Abfrage in der Frage Hash-Übereinstimmungen (Aggregate) anstelle von Hash-Übereinstimmungen (Flow-distinct)? Da sich die Anzahl der eindeutigen Werte in der Tabelle ändert, würde ich davon ausgehen, dass die Kosten für die Hash-Übereinstimmungsabfrage (flow distinct) sinken, da die Schätzung der Anzahl der Zeilen, die in die Tabelle gescannt werden müssen, sinken sollte. Ich würde erwarten, dass die Kosten für den Hash-Match-Plan (aggregiert) steigen, da die zu erstellende Hash-Tabelle größer wird. Eine Möglichkeit, dies zu untersuchen, besteht darin , einen Planungsleitfaden zu erstellen . Wenn ich zwei Kopien der Daten erstelle, aber einen Planleitfaden auf eine von ihnen anwende, sollte es mir möglich sein, die Hash-Übereinstimmung (aggregiert) mit der Hash-Übereinstimmung (eindeutig) nebeneinander mit denselben Daten zu vergleichen. Beachten Sie, dass ich dazu die Regeln des Abfrageoptimierers nicht deaktivieren kann, da für beide Pläne dieselbe Regel gilt ( GbAggToHS
).
Hier ist eine Möglichkeit, den Plan zu finden, nach dem ich suche:
DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;
CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);
UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;
-- run this query
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Holen Sie sich das Planhandle und erstellen Sie daraus einen Planleitfaden:
-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM
sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;
EXEC sp_create_plan_guide_from_handle
'EVIL_PLAN_GUIDE',
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;
Planhinweise arbeiten nur mit dem genauen Abfragetext. Kopieren Sie ihn also aus dem Planhinweis zurück:
SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';
Setzen Sie die Daten zurück:
TRUNCATE TABLE X_PLAN_GUIDE_TARGET;
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);
Abrufen eines Abfrageplans für die Abfrage mit angewendetem Planungshandbuch:
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Dies hat den Hash-Match-Operator (flow distinct), den wir mit unseren Testdaten wollten. Beachten Sie, dass SQL Server erwartet, alle Zeilen aus der Tabelle zu lesen, und dass die geschätzten Kosten genau die gleichen sind wie für den Plan mit der Hash-Übereinstimmung (Aggregat). Die von mir durchgeführten Tests haben ergeben, dass die Kosten für die beiden Pläne identisch sind, wenn das Zeilenziel für den Plan größer oder gleich der Anzahl unterschiedlicher Werte ist, die SQL Server aus der Tabelle erwartet, die in diesem Fall einfach aus der Tabelle abgeleitet werden kann Statistiken. Leider wählt der Optimierer (für unsere Abfrage) die Hash-Übereinstimmung (aggregiert) über die Hash-Übereinstimmung (flussunabhängig), wenn die Kosten gleich sind. Wir sind also 0,0000001 Magic Optimizer-Einheiten vom gewünschten Plan entfernt.
Eine Möglichkeit, dieses Problem anzugehen, besteht darin, das Zeilenziel zu verringern. Wenn das Zeilenziel aus der Sicht des Optimierers kleiner ist als die eindeutige Anzahl der Zeilen, erhalten wir wahrscheinlich eine Hash-Übereinstimmung (Flow distinct). Dies kann mit dem OPTIMIZE FOR
Abfragehinweis erreicht werden:
DECLARE @j INT = 10;
SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));
Für diese Abfrage erstellt der Optimierer einen Plan, als ob die Abfrage nur die erste Zeile benötigt, aber wenn die Abfrage ausgeführt wird, werden die ersten 10 Zeilen zurückgegeben. Auf meinem Computer durchsucht diese Abfrage 892800 Zeilen X_10_DISTINCT_HEAP
und wird in 299 ms mit 250 ms CPU-Zeit und 2537 logischen Lesevorgängen abgeschlossen.
Beachten Sie, dass diese Technik nicht funktioniert, wenn die Statistik nur einen bestimmten Wert ausgibt. Dies kann bei Stichprobenstatistiken mit verzerrten Daten der Fall sein. In diesem Fall ist es jedoch unwahrscheinlich, dass Ihre Daten dicht genug gepackt sind, um die Verwendung solcher Techniken zu rechtfertigen. Sie verlieren möglicherweise nicht viel, wenn Sie alle Daten in der Tabelle scannen, insbesondere wenn dies parallel erfolgen kann.
Eine andere Möglichkeit, dieses Problem zu bekämpfen, besteht darin, die Anzahl der geschätzten unterschiedlichen Werte zu erhöhen, die SQL Server von der Basistabelle erwartet. Das war schwieriger als erwartet. Das Anwenden einer deterministischen Funktion kann möglicherweise die eindeutige Anzahl der Ergebnisse nicht erhöhen. Wenn dem Abfrageoptimierer diese mathematische Tatsache bekannt ist (einige Tests legen dies zumindest für unsere Zwecke nahe), erhöht die Anwendung deterministischer Funktionen (die alle Zeichenfolgenfunktionen enthalten ) nicht die geschätzte Anzahl unterschiedlicher Zeilen.
Viele der nicht deterministischen Funktionen funktionierten auch nicht, einschließlich der offensichtlichen Auswahl von NEWID()
und RAND()
. Tut LAG()
jedoch den Trick für diese Abfrage. Das Abfrageoptimierungsprogramm erwartet 10 Millionen verschiedene Werte für den LAG
Ausdruck, wodurch ein Hash-Übereinstimmungsplan (flow distinct) ausgelöst wird :
SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
Auf meinem Computer durchsucht diese Abfrage 892800 Zeilen X_10_DISTINCT_HEAP
und wird in 1165 ms mit 1109 ms CPU-Zeit und 2537 logischen Lesevorgängen abgeschlossen LAG()
. @Paul White schlug vor, die Stapelverarbeitung für diese Abfrage zu versuchen. In SQL Server 2016 können wir sogar mit Stapelverarbeitung arbeiten MAXDOP 1
. Eine Möglichkeit, eine Stapelverarbeitung für eine Rowstore-Tabelle zu erhalten, besteht darin, eine leere CCI wie folgt zu verknüpfen:
CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);
CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;
SELECT DISTINCT TOP 10 VAL
FROM
(
SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
FROM X_10_DISTINCT_HEAP
LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);
Dieser Code führt zu diesem Abfrageplan .
Paul wies darauf hin, dass ich die zu verwendende Abfrage ändern musste, LAG(..., 1)
da LAG(..., 0)
anscheinend keine Berechtigung für die Fensteraggregatoptimierung besteht. Diese Änderung reduzierte die verstrichene Zeit auf 520 ms und die CPU-Zeit auf 454 ms.
Beachten Sie, dass der LAG()
Ansatz nicht der stabilste ist. Wenn Microsoft die Eindeutigkeitsannahme für die Funktion ändert, funktioniert sie möglicherweise nicht mehr. Es hat eine andere Schätzung mit dem Erbe CE. Auch diese Art der Optimierung gegen einen Haufen ist keine gute Idee. Wenn die Tabelle neu erstellt wird, kann es im schlimmsten Fall vorkommen, dass fast alle Zeilen aus der Tabelle gelesen werden müssen.
Gegenüber einer Tabelle mit einer eindeutigen Spalte (wie dem Beispiel für einen gruppierten Index in der Frage) haben wir bessere Optionen. Zum Beispiel können wir den Optimierer überlisten, indem wir einen SUBSTRING
Ausdruck verwenden, der immer eine leere Zeichenfolge zurückgibt. SQL Server geht nicht davon aus, dass dies die SUBSTRING
Anzahl der unterschiedlichen Werte ändert. Wenn Sie diese also auf eine eindeutige Spalte wie PK anwenden, beträgt die geschätzte Anzahl der unterschiedlichen Zeilen 10 Millionen. Diese folgende Abfrage ruft den Hash-Match-Operator (flow distinct) ab:
SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);
Auf meinem Computer durchsucht diese Abfrage 900000 Zeilen X_10_DISTINCT_CI
und wird in 333 ms mit 297 ms CPU-Zeit und 3011 logischen Lesevorgängen abgeschlossen.
Zusammenfassend scheint das Abfrageoptimierungsprogramm davon auszugehen, dass bei SELECT DISTINCT TOP N
Abfragen alle Zeilen aus der Tabelle gelesen werden, wenn N
> = die Anzahl der geschätzten unterschiedlichen Zeilen aus der Tabelle ist. Der Hash-Übereinstimmungsoperator (Aggregatoperator) kann die gleichen Kosten verursachen wie der Hash-Übereinstimmungsoperator (Flow-distinct-Operator), der Optimierer wählt jedoch immer den Aggregatoperator aus. Dies kann zu unnötigen logischen Lesevorgängen führen, wenn sich zu Beginn der Tabellensuche genügend unterschiedliche Werte befinden. Sie können den Optimierer auf zwei Arten dazu verleiten, den Hash-Match-Operator (Flow distinct) zu verwenden, indem Sie das OPTIMIZE FOR
Zeilenziel mithilfe des Hinweises verringern oder die geschätzte Anzahl unterschiedlicher Zeilen mithilfe von LAG()
oder SUBSTRING
in einer eindeutigen Spalte erhöhen .