CROSS APPLY produce un join esterno


17

In risposta al conteggio SQL distinto sulla partizione, Erik Darling ha pubblicato questo codice per ovviare alla mancanza di COUNT(DISTINCT) OVER ():

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;

La query utilizza CROSS APPLY(non OUTER APPLY) quindi perché esiste un join esterno nel piano di esecuzione anziché un join interno ?

inserisci qui la descrizione dell'immagine

Inoltre, perché la decompressione del gruppo in base alla clausola si traduce in un join interno?

inserisci qui la descrizione dell'immagine

Non penso che i dati siano importanti, ma copiarli da quelli dati da Kevin sull'altra domanda:

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)

Risposte:


23

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 BYclausola corrispondente è un aggregato scalare .
  • Un aggregato con una GROUP BYclausola 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' COUNTaggregato 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 BYnella tabella derivata (altrimenti non ci potrebbe essere una Acolonna su cui unirsi). Il join deve essere un join esterno in modo che ogni riga dalla tabella @Acontinui a produrre una riga nell'output. Il join sinistro produrrà una NULLcolonna for cquando il predicato del join non restituisce true. Questo NULLdeve essere tradotto a zero COALESCEper completare una trasformazione corretta da applicare .

La demo di seguito mostra come join sia esterno e COALESCEsono 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_BIGora è 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 APPLYrifiuta 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 ApplyHandlerecc.) 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):

Spool indice applica piano

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 BYclausola.

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


-3

L'applicazione incrociata è un'operazione logica sui dati. Quando decide come ottenere tali dati, SQL Server sceglie l'operatore fisico appropriato per ottenere i dati desiderati.

Non esiste un operatore di applicazione fisica e SQL Server lo traduce nell'operatore di join appropriato e, si spera, efficiente.

È possibile trovare un elenco degli operatori fisici nel collegamento seguente.

https://docs.microsoft.com/en-us/sql/relational-databases/showplan-logical-and-physical-operators-reference?view=sql-server-2017

Query Optimizer crea un piano di query come albero costituito da operatori logici. Dopo che Query Optimizer ha creato il piano, Query Optimizer sceglie l'operatore fisico più efficiente per ciascun operatore logico. Query Optimizer utilizza un approccio basato sui costi per determinare quale operatore fisico implementerà un operatore logico.

Di solito, un'operazione logica può essere implementata da più operatori fisici. Tuttavia, in rari casi, un operatore fisico può anche implementare più operazioni logiche.

modifica / Sembra che ho capito male la tua domanda. Il server SQL normalmente sceglierà l'operatore più appropriato. La tua query non ha bisogno di restituire valori per tutte le combinazioni di entrambe le tabelle, quando si userebbe un cross join. Basta calcolare il valore desiderato per ogni riga, che è ciò che viene fatto qui.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.