it-swarm.com.de

CROSS APPLY erzeugt eine äußere Verbindung

Als Antwort auf SQL-Zählung deutlich über Partition Erik Darling hat diesen Code gepostet, um das Fehlen von COUNT(DISTINCT) OVER () zu umgehen:

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

Die Abfrage verwendet CROSS APPLY (nicht OUTER APPLY) Warum gibt es also einen äußeren Join im Ausführungsplan anstelle eines inneren Joins?

(enter image description here

Warum führt das Auskommentieren der group by-Klausel zu einem inneren Join?

(enter image description here

Ich denke nicht, dass die Daten wichtig sind, aber ich kopiere sie von Kevin What auf die andere Frage:

create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)
18
Paul White 9

Zusammenfassung

SQL Server verwendet den richtigen Join (innen oder außen) und fügt bei Bedarf Projektionen hinzu, um die gesamte Semantik der ursprünglichen Abfrage bei der Ausführung von interne Übersetzungen zwischen = zu berücksichtigen bewerben und beitreten.

Die Unterschiede in den Plänen können alle durch die nterschiedliche Semantik von Aggregaten mit und ohne Group by-Klausel in SQL Server erklärt werden.


Einzelheiten

Join vs Apply

Wir müssen in der Lage sein, zwischen einem anwenden und einem beitreten zu unterscheiden:

  • Übernehmen

    Der innere (untere) Eingang des apply wird für jede Zeile des äußeren (oberen) Eingangs ausgeführt, wobei ein oder mehrere innere Seitenparameterwerte von der aktuellen äußeren Reihe bereitgestellt werden. Das Gesamtergebnis von apply ist die Kombination (Vereinigung aller) aller Zeilen, die durch die parametrisierten Ausführungen auf der Innenseite erzeugt werden. Das Vorhandensein von Parametern bedeutet, dass anwenden manchmal als korrelierte Verknüpfung bezeichnet wird.

    Ein apply wird in Ausführungsplänen immer vom Operator Nested Loops implementiert. Der Operator verfügt über eine Outer References -Eigenschaft, anstatt Prädikate zu verbinden. Die äußeren Referenzen sind die Parameter, die bei jeder Iteration der Schleife von der Außenseite zur Innenseite übergeben werden.

  • Join

    Ein Join wertet sein Join-Prädikat beim Join-Operator aus. Der Join kann im Allgemeinen durch Operatoren Hash Match, Merge oder Nested Loops in SQL Server implementiert werden.

    Wenn Verschachtelte Schleifen ausgewählt ist, kann dies von einem Anwenden durch das Fehlen von Äußeren Referenzen (und normalerweise der Vorhandensein eines Join-Prädikats). Die innere Eingabe eines join verweist niemals auf Werte aus der äußeren Eingabe - die Innenseite wird immer noch einmal für jede äußere Zeile ausgeführt, aber die Ausführung der Innenseite hängt nicht von Werten aus der aktuellen äußeren Zeile ab .

Weitere Details finden Sie in meinem Beitrag Anwenden versus Verschachteln von verschachtelten Schleifen .

... warum gibt es im Ausführungsplan einen äußeren Join anstelle eines inneren Joins?

Der äußere Join entsteht, wenn der Optimierer einen apply In einen join (unter Verwendung einer Regel namens ApplyHandler) umwandelt, um zu sehen, ob er einen finden kann billigerer Join-basierter Plan. Der Join muss ein äußerer Join für Korrektheit sein, wenn der Apply Ein Skalaraggregat enthält. Ein innerer Join wäre nicht garantiert, um die gleichen Ergebnisse wie das Original zu erzielen anwenden 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 Skalar Aggregat immer eine Zeile, auch wenn keine zu aggregierenden Zeilen angegeben sind. Beispielsweise ist das skalare COUNT Aggregat ohne Zeilen Null. Ein VektorCOUNT Aggregat ohne Zeilen ist die leere Menge (überhaupt keine Zeilen).

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 <> Geigen-Demo

Transformieren gilt für den Beitritt

Ich habe zuvor erwähnt, dass der Join ein äußerer Join für Korrektheit sein muss, wenn das Original anwenden ein skalares Aggregat enthält . Um zu zeigen, warum dies im Detail der Fall ist, werde ich ein vereinfachtes Beispiel für die 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 korrekte Ergebnis für die Spalte c ist Null , da COUNT_BIG Ein skalares Aggregat ist . Bei der Übersetzung dieser Apply-Abfrage in ein Join-Formular 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 die Anwendung als unkorrelierten Join neu zu schreiben, müssen wir ein GROUP BY In die abgeleitete Tabelle einfügen (andernfalls könnte es keine A -Spalte geben, an der wir teilnehmen können). Der Join muss ein äußerer Join sein, damit jede Zeile aus Tabelle @A Weiter eine Zeile in der Ausgabe erzeugt. Der linke Join erzeugt ein NULL für die Spalte c, wenn das Join-Prädikat nicht als wahr ausgewertet wird. Das NULL muss von COALESCE auf Null übersetzt werden, um eine korrekte Transformation von apply abzuschließen.

Die folgende Demo zeigt, wie sowohl Outer Join als auch COALESCE erforderlich sind, um dieselben Ergebnisse zu erzielen, indem join wie die ursprüngliche Abfrage apply verwendet wird:

db <> Geigen-Demo

Mit dem GROUP BY

... warum führt das Kommentieren der group by-Klausel zu einem inneren Join?

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 jetzt ein Vektoraggregat , sodass das korrekte Ergebnis für eine leere Eingabemenge nicht mehr Null ist, sondern keine Zeile mehr überhaupt. Mit anderen Worten, wenn Sie die obigen Anweisungen ausführen, wird keine Ausgabe erzeugt.

Diese Semantik ist bei der Übersetzung von apply nach join viel einfacher zu berücksichtigen, da CROSS APPLY Natürlich jede äußere Zeile ablehnt, die keine inneren Seitenreihen erzeugt . Wir können daher jetzt sicher einen inneren Join 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 der inneren Verknüpfung dieselben Ergebnisse liefert wie das ursprüngliche Anwenden mit Vektoraggregat:

db <> Geigen-Demo

Der Optimierer wählt zufällig einen inneren Merge-Join mit der kleinen Tabelle aus, weil er schnell einen billigen join Plan findet (gut genug gefundener Plan). Der kostenbasierte Optimierer schreibt den Join möglicherweise wieder in einen Antrag um - möglicherweise findet er einen günstigeren Anwendungsplan, wie dies hier der Fall ist, wenn ein Loop-Join oder ein Forceseek-Hinweis verwendet wird -, aber in diesem Fall lohnt sich der Aufwand nicht.

Anmerkungen

Die vereinfachten Beispiele verwenden unterschiedliche Tabellen mit unterschiedlichen Inhalten, 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-Joining) Zeilen erzeugen kann, aber er enthält diese Logik heute nicht. Der mehrmalige Zugriff auf dieselbe Tabelle in einer Abfrage führt im Allgemeinen ohnehin nicht zu denselben Ergebnissen, abhängig von der Isolationsstufe und der gleichzeitigen Aktivität.

Der Optimierer kümmert sich um diese Semantik und Edge-Fälle, sodass Sie dies nicht tun müssen.


Bonus: Inner Bewerben Planen

SQL Server kann einen inneren anwenden Plan (kein innerer join Plan für die Beispielabfrage erstellen , es entscheidet sich einfach aus Kostengründen dagegen. Die Kosten für den in der Frage gezeigten äußeren Join-Plan betragen 0,02898 Einheiten auf der SQL Server 2017-Instanz meines Laptops.

Sie können einen anwenden (korrelierten Join) Plan erzwingen, indem Sie das undokumentierte und nicht unterstützte Trace-Flag 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 einen apply Plan für verschachtelte Schleifen mit einer Lazy-Index-Spool. Die geschätzten Gesamtkosten betragen 0,0463983 (höher als der ausgewählte Plan):

(Index Spool apply plan

Beachten Sie, dass der Ausführungsplan mit apply Verschachtelten Schleifen korrekte Ergebnisse mit der Semantik "inner join" liefert, unabhängig vom Vorhandensein der Klausel GROUP BY.

In der realen Welt haben wir normalerweise einen Index, der eine Suche auf der Innenseite von apply unterstützt, um SQL Server zu ermutigen, diese Option natürlich zu wählen, zum Beispiel:

CREATE INDEX i ON #MyTable (Col_A, Col_B);

db <> Geigen-Demo

24
Paul White 9