Zusammenfassung
SQL Server verwendet die richtige Verknüpfung (innere oder äußere) und fügt bei Bedarf Projektionen hinzu, um alle Semantiken der ursprünglichen Abfrage zu berücksichtigen, wenn interne Übersetzungen zwischen Anwenden und Verknüpfen ausgeführt werden .
Die Unterschiede in den Plänen lassen sich alle durch die unterschiedliche Semantik von Aggregaten mit und ohne Group-by-Klausel in SQL Server erklären .
Einzelheiten
Join vs Apply
Wir müssen in der Lage sein, zwischen einem Apply und einem Join zu unterscheiden :
Anwenden
Die innere (untere) Eingabe von " apply" wird für jede Zeile der äußeren (oberen) Eingabe ausgeführt, wobei ein oder mehrere innere Seitenparameterwerte von der aktuellen äußeren Zeile bereitgestellt werden. Das Gesamtergebnis der Anwendung ist die Kombination (Vereinigung aller) aller Zeilen, die durch die parametrisierten Ausführungen der Innenseite erzeugt werden. Das Vorhandensein von Parametern bedeutet, dass apply manchmal als korrelierter Join bezeichnet wird.
Ein Apply wird in Ausführungsplänen immer vom Nested Loops- Operator implementiert . Der Operator verfügt über eine Outer References- Eigenschaft, anstatt Prädikate zu verknüpfen. Die äußeren Referenzen sind die Parameter, die bei jeder Iteration der Schleife von der Außenseite zur Innenseite übergeben werden.
Beitreten
Ein Join wertet sein Join-Prädikat beim Join-Operator aus. Der Join kann im Allgemeinen von Hash Match- , Merge- oder Nested Loops- Operatoren in SQL Server implementiert werden .
Wenn verschachtelte Schleifen ausgewählt werden, kann dies durch das Fehlen äußerer Referenzen (und normalerweise durch das Vorhandensein eines Join-Prädikats) von einer Anwendung unterschieden werden . Die innere Eingabe eines Joins verweist nie auf Werte aus der äußeren Eingabe. Die innere Seite wird immer noch einmal für jede äußere Zeile ausgeführt, aber die Ausführung der inneren Seite hängt nicht von Werten aus der aktuellen äußeren Zeile ab.
Weitere Details finden Sie in meinem Beitrag Apply versus Nested Loops Join .
... warum gibt es im Ausführungsplan eine äußere Verknüpfung anstelle einer inneren Verknüpfung?
Die äußere Verknüpfung entsteht, wenn das Optimierungsprogramm eine Anwendung auf eine Verknüpfung (unter Verwendung einer aufgerufenen Regel ApplyHandler
) umwandelt , um festzustellen, ob ein kostengünstiger auf Verknüpfungen basierender Plan gefunden werden kann. Die Verknüpfung muss eine äußere Verknüpfung sein, um die Richtigkeit zu gewährleisten, wenn die Anwendung ein skalares Aggregat enthält . Es kann nicht garantiert werden , dass eine innere Verknüpfung die gleichen Ergebnisse wie die ursprüngliche Verknüpfung liefert, wie wir sehen werden.
Skalar- und Vektoraggregate
- Ein Aggregat ohne entsprechende
GROUP BY
Klausel ist ein skalares Aggregat.
- Ein Aggregat mit einer entsprechenden
GROUP BY
Klausel ist ein Vektor - Aggregat.
In SQL Server erzeugt ein skalares Aggregat immer eine Zeile, auch wenn es keine zu aggregierenden Zeilen erhält. Beispielsweise ist das skalare COUNT
Aggregat ohne Zeilen Null. Ein Vektor COUNT
- Aggregat keine Zeilen ist die leere Menge (keine Zeilen überhaupt).
Die folgenden Spielzeugabfragen veranschaulichen den Unterschied. Weitere Informationen zu Skalar- und Vektoraggregaten finden Sie in meinem Artikel Spaß mit Skalar- und Vektoraggregaten .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
db <> Geige Demo
Transformieren anwenden, um beizutreten
Ich habe bereits erwähnt, dass der Join ein äußerer Join sein muss, damit er korrekt ist, wenn das ursprüngliche Apply ein skalares Aggregat enthält . Um zu zeigen, warum dies im Detail der Fall ist, werde ich ein vereinfachtes Beispiel der Fragenabfrage verwenden:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
Das richtige Ergebnis für die Spalte c
ist Null , da COUNT_BIG
es sich um ein skalares Aggregat handelt. Bei der Übersetzung dieser Apply-Abfrage für das Beitrittsformular generiert SQL Server eine interne Alternative, die wie folgt aussehen würde, wenn sie in T-SQL ausgedrückt würde:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Um das Apply als unkorrelierten Join umzuschreiben, müssen Sie ein GROUP BY
in die abgeleitete Tabelle einfügen (andernfalls ist möglicherweise keine A
Spalte zum Verknüpfen vorhanden). Der Join muss ein Outer- Join sein, damit jede Zeile aus der Tabelle @A
weiterhin eine Zeile in der Ausgabe erzeugt. Der linke Join erzeugt eine NULL
for-Spalte, c
wenn das Join-Prädikat nicht mit true ausgewertet wird. Das NULL
muss bis auf Null übersetzt werden COALESCE
, um eine korrekte Transformation von Apply abzuschließen .
In der folgenden Demo wird gezeigt, wie sowohl Outer Join als auch Outer Join COALESCE
erforderlich sind, um dieselben Ergebnisse mit Join als ursprünglicher Apply- Abfrage zu erzielen :
db <> Geige Demo
Mit dem GROUP BY
... warum führt das Auskommentieren der group by-Klausel zu einer inneren Verknüpfung?
Fortsetzung des vereinfachten Beispiels, aber Hinzufügen eines GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
Das COUNT_BIG
ist nun ein Vektor Aggregat, so dass das richtige Ergebnis für ein leeres Eingabemenge ist nicht mehr Null ist , ist es keine Zeile überhaupt . Mit anderen Worten, das Ausführen der obigen Anweisungen führt zu keiner Ausgabe.
Diese Semantik ist viel einfacher zu berücksichtigen, wenn Sie vom Anwenden zum Verbinden übersetzen , da CROSS APPLY
natürlich jede äußere Zeile abgelehnt wird, die keine inneren Seitenzeilen erzeugt. Aus diesem Grund können wir jetzt eine innere Verknüpfung ohne zusätzliche Ausdrucksprojektion verwenden:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Die folgende Demo zeigt, dass das Umschreiben des inneren Joins dieselben Ergebnisse wie das ursprüngliche Anwenden mit Vektoraggregat liefert:
db <> Geige Demo
Das Optimierungsprogramm wählt zufällig einen Merge-Inner-Join mit der kleinen Tabelle, da es schnell einen günstigen Join- Plan findet (gut genug, um einen Plan zu finden). Das kostenbasierte Optimierungsprogramm schreibt den Join möglicherweise wieder in einen Apply um - möglicherweise wird ein günstigerer Apply-Plan gefunden, wie dies hier der Fall ist, wenn ein Loop-Join oder Forceseek-Hinweis verwendet wird - aber in diesem Fall lohnt sich der Aufwand nicht.
Anmerkungen
In den vereinfachten Beispielen werden verschiedene Tabellen mit unterschiedlichen Inhalten verwendet, um die semantischen Unterschiede deutlicher darzustellen.
Man könnte argumentieren, dass der Optimierer in der Lage sein sollte, zu argumentieren, dass ein Self-Join keine nicht übereinstimmenden (nicht verbundenen) Zeilen erzeugen kann, aber er enthält diese Logik heute nicht. Es ist nicht garantiert, dass ein mehrmaliger Zugriff auf dieselbe Tabelle in einer Abfrage im Allgemeinen zu denselben Ergebnissen führt, je nach Isolationsstufe und gleichzeitiger Aktivität.
Das Optimierungsprogramm kümmert sich um diese Semantik und Kantenfälle, sodass Sie dies nicht tun müssen.
Bonus: Inner Apply Plan
SQL Server kann einen inneren Anwendungsplan (keinen inneren Verknüpfungsplan !) Für die Beispielabfrage erstellen. Aus Kostengründen wird nur darauf verzichtet. Die Kosten für den in der Frage gezeigten Outer Join-Plan betragen 0,02898 Einheiten auf der SQL Server 2017-Instanz meines Laptops.
Sie können einen Apply- Plan (korrelierter Join) erzwingen , indem Sie das nicht dokumentierte und nicht unterstützte Ablaufverfolgungsflag 9114 (das ApplyHandler
usw. deaktiviert ) nur zur Veranschaulichung verwenden:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Dies erzeugt eine Anwendung verschachtelte Schleifen Plans mit einer faulen Index Spule. Die geschätzten Gesamtkosten betragen 0,0463983 (höher als der ausgewählte Plan):
Beachten Sie, dass der Ausführungsplan, bei dem verschachtelte Schleifen angewendet werden, unter Verwendung der Semantik "Inner Join" korrekte Ergebnisse liefert, unabhängig vom Vorhandensein der GROUP BY
Klausel.
In der realen Welt verfügen wir normalerweise über einen Index, der eine Suche auf der Innenseite der Anwendung unterstützt , um SQL Server zu ermutigen, diese Option auf natürliche Weise zu wählen. Beispiel:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
db <> Geige Demo