Ho letto spesso quando si doveva verificare l'esistenza di una riga dovrebbe sempre essere fatto con EXISTS anziché con un COUNT.
È molto raro che qualsiasi cosa sia sempre vera, specialmente quando si tratta di database. Esistono molti modi per esprimere lo stesso semantico in SQL. Se esiste una regola empirica utile, potrebbe essere quella di scrivere query utilizzando la sintassi più naturale disponibile (e, sì, che è soggettiva) e considerare la riscrittura solo se il piano di query o le prestazioni ottenute sono inaccettabili.
Per quello che vale, la mia opinione sul problema è che le query di esistenza sono espresse in modo più naturale usando EXISTS
. E 'stata anche la mia esperienza che EXISTS
tende ad ottimizzare al meglio rispetto al OUTER JOIN
scarto NULL
alternativa. L'utilizzo COUNT(*)
e il filtro su =0
sono un'altra alternativa, che sembra avere un po 'di supporto in Query Optimizer di SQL Server, ma ho trovato personalmente che questo non è affidabile nelle query più complesse. In ogni caso, EXISTS
sembra molto più naturale (per me) di una di quelle alternative.
Mi chiedevo se ci fosse un difetto non araldico con EXISTS che ha dato perfettamente senso alle misurazioni che ho fatto
Il tuo esempio particolare è interessante, perché evidenzia il modo in cui l'ottimizzatore gestisce le subquery nelle CASE
espressioni (e EXISTS
in particolare i test).
Sottoquery nelle espressioni CASE
Considera la seguente query (perfettamente legale):
DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);
SELECT
CASE
WHEN (SELECT W.b FROM @When AS W) = 1
THEN (SELECT T.c FROM @Then AS T)
ELSE (SELECT E.d FROM @Else AS E)
END
FROM @Base AS B;
La semantica diCASE
è che le WHEN/ELSE
clausole sono generalmente valutate in ordine testuale. Nella query precedente, sarebbe errato che SQL Server restituisse un errore se la ELSE
subquery restituiva più di una riga, se la WHEN
clausola era soddisfatta. Per rispettare queste semantiche, l'ottimizzatore produce un piano che utilizza predicati pass-through:
Il lato interno dei join di loop nidificati viene valutato solo quando il predicato pass-through restituisce false. L'effetto complessivo è che le CASE
espressioni vengono testate in ordine e le sottoquery vengono valutate solo se nessuna espressione precedente è stata soddisfatta.
Espressioni CASE con una sottoquery EXISTS
Laddove viene CASE
utilizzata una sottoquery EXISTS
, il test di esistenza logica viene implementato come semi-join, ma le righe che sarebbero normalmente rifiutate dal semi-join devono essere conservate nel caso in cui una clausola successiva ne abbia bisogno. Le righe che attraversano questo speciale tipo di semi-join acquisiscono una bandiera per indicare se il semi-join ha trovato una corrispondenza o meno. Questo flag è noto come colonna probe .
I dettagli dell'implementazione sono che la sottoquery logica è sostituita da un join correlato ('applica') con una colonna probe. Il lavoro viene eseguito da una regola di semplificazione in Query Optimizer chiamata RemoveSubqInPrj
(rimuove la subquery nella proiezione). Possiamo vedere i dettagli usando il flag di traccia 8606:
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);
Di EXISTS
seguito è mostrata una parte dell'albero di input che mostra il test:
ScaOp_Exists
LogOp_Project
LogOp_Select
LogOp_Get TBL: #T2
ScaOp_Comp x_cmpEq
ScaOp_Identifier [T2].ID
ScaOp_Identifier [T1].ID
Questo si trasforma RemoveSubqInPrj
in una struttura guidata da:
LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)
Questo è il semi-join sinistro applicato con sonda descritta in precedenza. Questa trasformazione iniziale è l'unica disponibile finora negli ottimizzatori di query di SQL Server e la compilazione fallirà semplicemente se questa trasformazione è disabilitata.
Una delle possibili forme del piano di esecuzione per questa query è un'implementazione diretta di quella struttura logica:
Il calcolo scalare finale valuta il risultato CASE
dell'espressione usando il valore della colonna della sonda:
La forma base dell'albero del piano viene mantenuta quando l'ottimizzazione considera altri tipi di join fisici per il semi-join. Solo unisci unione supporta una colonna probe, quindi un semi join hash, sebbene logicamente possibile, non viene considerato:
Si noti che l'unione genera un'espressione etichettata Expr1008
(che il nome è lo stesso di prima è una coincidenza) anche se nessuna definizione appare su qualsiasi operatore nel piano. Questa è di nuovo solo la colonna della sonda. Come prima, il calcolo scalare finale utilizza questo valore di sonda per valutare il CASE
.
Il problema è che l'ottimizzatore non esplora completamente le alternative che diventano utili solo con l'unione (o hash) semi join. Nel piano dei cicli nidificati, non vi è alcun vantaggio nel controllare se le righe in T2
corrispondenza corrispondono all'intervallo su ogni iterazione. Con un piano di unione o hash, questa potrebbe essere un'ottimizzazione utile.
Se aggiungiamo un BETWEEN
predicato corrispondente a T2
nella query, tutto ciò che accade è che questo controllo viene eseguito per ogni riga come residuo sul semi join di unione (difficile da individuare nel piano di esecuzione, ma è lì):
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
AND T2.ID BETWEEN 5000 AND 7000 -- New
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Spereremmo che il BETWEEN
predicato venga invece spinto verso il basso, T2
risultando in una ricerca. Normalmente, l'ottimizzatore prenderebbe in considerazione di farlo (anche senza il predicato aggiuntivo nella query). Riconosce i predicati impliciti ( BETWEEN
on T1
e il predicato join tra T1
e T2
insieme implicano BETWEEN
on T2
) senza che siano presenti nel testo della query originale. Sfortunatamente, il modello di applicare la sonda significa che questo non viene esplorato.
Esistono modi per scrivere la query per produrre ricerche su entrambi gli input in un join semi join. Un modo consiste nello scrivere la query in un modo abbastanza innaturale (sconfiggendo il motivo che generalmente preferisco EXISTS
):
WITH T2 AS
(
SELECT TOP (9223372036854775807) *
FROM #T2 AS T2
WHERE ID BETWEEN 5000 AND 7000
)
SELECT
T1.ID,
DoesExist =
CASE
WHEN EXISTS
(
SELECT * FROM T2
WHERE T2.ID = T1.ID
) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Non sarei felice di scrivere quella query in un ambiente di produzione, è solo per dimostrare che la forma del piano desiderata è possibile. Se la query reale che è necessario scrivere utilizza CASE
in questo modo particolare e le prestazioni risentono del fatto che non esiste una ricerca sul lato sonda di un semi-join di unione, è possibile prendere in considerazione la possibilità di scrivere la query utilizzando una sintassi diversa che produca i risultati giusti e un piano di esecuzione più efficiente.