Sommario
SQL Server utilizza il join corretto (interno o esterno) e aggiunge le proiezioni ove necessario per onorare tutta la semantica della query originale quando si eseguono traduzioni interne tra applicare e unire .
Le differenze nei piani possono essere spiegate dalla diversa semantica degli aggregati con e senza una clausola group by in SQL Server.
Dettagli
Iscriviti vs Applica
Dovremo essere in grado di distinguere tra un candidato e un join :
Applicare
L'ingresso interno (inferiore) dell'applicazione viene eseguito per ciascuna riga dell'ingresso esterno (superiore), con uno o più valori dei parametri del lato interno forniti dalla riga esterna corrente. Il risultato complessivo dell'applicazione è la combinazione (unione di tutti) di tutte le file prodotte dalle esecuzioni parametriche del lato interno. La presenza di parametri significa che l' applicazione è a volte indicata come join correlato.
Un applica è sempre implementato in piani di esecuzione da parte nidificati Loops dell'operatore. L'operatore avrà una proprietà Riferimenti esterni anziché unire predicati. I riferimenti esterni sono i parametri passati dal lato esterno al lato interno su ciascuna iterazione del ciclo.
Aderire
Un join valuta il suo predicato di join presso l'operatore di join. Il join può essere generalmente implementato dagli operatori Hash Match , Merge o Nested Loops in SQL Server.
Quando Cicli annidati viene scelto, può essere distinto da un diffusa dalla mancanza di Riferimenti esterni (e di solito la presenza di un join predicato). L'input interno di un join non fa mai riferimento ai valori dell'input esterno: il lato interno viene comunque eseguito una volta per ogni riga esterna, ma le esecuzioni del lato interno non dipendono da alcun valore della riga esterna corrente.
Per maggiori dettagli vedi il mio post Applica contro Nested Loops Join .
... perché esiste un join esterno nel piano di esecuzione anziché un join interno ?
L'outer join sorge quando l'ottimizzatore trasforma un applicano a un join (utilizzando una regola chiamata ApplyHandler
) per vedere se si può trovare un piano join-base più conveniente. Il join deve essere un join esterno per correttezza quando l' applicazione contiene un aggregato scalare . Un inner join non sarebbero garantiti per produrre gli stessi risultati come l'originale si applicano come vedremo.
Aggregati scalari e vettoriali
- Un aggregato senza una
GROUP BY
clausola corrispondente è un aggregato scalare .
- Un aggregato con una
GROUP BY
clausola corrispondente è un aggregato vettoriale .
In SQL Server, un aggregato scalare produrrà sempre una riga, anche se non viene fornita alcuna riga da aggregare. Ad esempio, l' COUNT
aggregato scalare di nessuna riga è zero. Un aggregato vettoriale COUNT
senza righe è l'insieme vuoto (nessuna riga).
Le seguenti domande sui giocattoli illustrano la differenza. Puoi anche leggere di più sugli aggregati scalari e vettoriali nel mio articolo Fun with Scalar and Vector Aggregates .
-- 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 <> demo violino
La trasformazione si applica per partecipare
Ho già detto che il join deve essere un join esterno per correttezza quando l' applicazione originale contiene un aggregato scalare . Per mostrare perché questo è il caso in dettaglio, userò un esempio semplificato della domanda:
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;
Il risultato corretto per la colonna c
è zero , poiché COUNT_BIG
è un aggregato scalare . Quando si traduce questa query applica in unire modulo, SQL Server genera un'alternativa interna che sarebbe simile alla seguente se fosse espressa in T-SQL:
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;
Per riscrivere l'applicazione come join non correlato, è necessario introdurre un GROUP BY
nella tabella derivata (altrimenti non ci potrebbe essere una A
colonna su cui unirsi). Il join deve essere un join esterno in modo che ogni riga dalla tabella @A
continui a produrre una riga nell'output. Il join sinistro produrrà una NULL
colonna for c
quando il predicato del join non restituisce true. Questo NULL
deve essere tradotto a zero COALESCE
per completare una trasformazione corretta da applicare .
La demo di seguito mostra come join sia esterno e COALESCE
sono tenuti a produrre gli stessi risultati utilizzando unirsi come l'originale si applicano query:
db <> demo violino
Con il GROUP BY
... perché la decompressione del gruppo in base alla clausola si traduce in un join interno?
Continuando l'esempio semplificato, ma aggiungendo un 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;
L' COUNT_BIG
ora è un vettore aggregato, quindi il risultato corretto per un insieme di input vuoto non è più zero, è nessuna riga affatto . In altre parole, l'esecuzione delle istruzioni precedenti non produce output.
Queste semantiche sono molto più facili da onorare quando si traduce da applicare in unire , poiché CROSS APPLY
rifiuta naturalmente qualsiasi riga esterna che non genera righe laterali interne. Ora possiamo quindi tranquillamente utilizzare un join interno, senza alcuna proiezione di espressioni extra:
-- 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;
La demo seguente mostra che la riscrittura del join interno produce gli stessi risultati dell'originale applicato con aggregato vettoriale:
db <> demo violino
L'ottimizzatore sembra scegliere un unire join interno con la tabella piccola perché trova rapidamente un piano di join economico (piano abbastanza buono trovato). L'ottimizzatore basato sui costi può continuare a riscrivere il join in una domanda - magari trovando un piano di applicazione più economico, come farebbe qui se viene utilizzato un loop loop o un suggerimento forceseek - ma non vale la pena in questo caso.
Appunti
Gli esempi semplificati usano tabelle diverse con contenuti diversi per mostrare più chiaramente le differenze semantiche.
Si potrebbe sostenere che l'ottimizzatore dovrebbe essere in grado di ragionare sul fatto che un self-join non sia in grado di generare righe non corrispondenti (non unite), ma non contiene quella logica oggi. L'accesso alla stessa tabella più volte in una query non garantisce comunque gli stessi risultati in generale, a seconda del livello di isolamento e dell'attività concorrente.
L'ottimizzatore si preoccupa per queste semantiche e casi limite quindi non è necessario.
Bonus: Inner Apply Plan
SQL Server può produrre un piano di applicazione interno (non un piano di join interno !) Per la query di esempio, ma sceglie di non farlo per motivi di costo. Il costo del piano di join esterno mostrato nella domanda è 0,02898 unità sull'istanza di SQL Server 2017 del mio laptop.
È possibile forzare un piano di applicazione (join correlato) utilizzando il flag di traccia 9114 non documentato e non supportato (che disabilita ApplyHandler
ecc.) Solo a scopo illustrativo:
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);
Ciò produce un piano di cicli nidificati di applicazione con una bobina di indice pigro. Il costo totale stimato è 0,0463983 (superiore al piano selezionato):
Si noti che il piano di esecuzione che utilizza i cicli nidificati applica produce risultati corretti usando la semantica "join interno" indipendentemente dalla presenza della GROUP BY
clausola.
Nel mondo reale, in genere avremmo un indice per supportare una ricerca sul lato interno dell'applicazione per incoraggiare SQL Server a scegliere questa opzione naturalmente, ad esempio:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
db <> demo violino