Controlla l'esistenza con EXISTS che supera COUNT! ... no?


36

Ho letto spesso quando si doveva verificare l'esistenza di una riga dovrebbe sempre essere fatto con EXISTS anziché con un COUNT.

Eppure in diversi scenari recenti ho misurato un miglioramento delle prestazioni durante l'utilizzo del conteggio.
Lo schema va così:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

Non ho familiarità con i metodi per dire cosa sta succedendo "all'interno" di SQL Server, quindi mi chiedevo se ci fosse un difetto non identificato con EXISTS che ha dato perfettamente senso alle misurazioni che ho fatto (EXISTS potrebbe essere RBAR ?!).

Hai qualche spiegazione a questi fenomeni?

MODIFICARE:

Ecco uno script completo che puoi eseguire:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

Su SQL Server 2008R2 (Seven 64bits) ottengo questo risultato

COUNT Versione:

Tabella '# 455F344D'. Conteggio scansioni 1, letture logiche 8, letture fisiche 0, letture avanti 0, letture logiche lob 0, letture fisiche lob 0, letture avanti lob legge 0.
Tabella '# 492FC531'. Conteggio scansioni 1, letture logiche 30, letture fisiche 0, letture avanti 0, letture logiche lob 0, letture fisiche lob 0, letture read lob 0.

Tempi di esecuzione di SQL Server:
tempo CPU = 0 ms, tempo trascorso = 81 ms.

EXISTS Versione:

Tabella '# 492FC531'. Conteggio scansioni 1, letture logiche 96, letture fisiche 0, letture avanti 0, letture
log lob 0, letture log lob 0, letture lob read 0. Tabella '# 455F344D'. Conteggio scansioni 1, letture logiche 8, letture fisiche 0, letture avanti 0, letture logiche lob 0, letture fisiche lob 0, letture read lob 0.

Tempi di esecuzione di SQL Server:
tempo CPU = 0 ms, tempo trascorso = 76 ms.

Risposte:


44

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 JOINscarto NULLalternativa. L'utilizzo COUNT(*)e il filtro su =0sono 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, EXISTSsembra 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 CASEespressioni (e EXISTSin 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/ELSEclausole sono generalmente valutate in ordine testuale. Nella query precedente, sarebbe errato che SQL Server restituisse un errore se la ELSEsubquery restituiva più di una riga, se la WHENclausola era soddisfatta. Per rispettare queste semantiche, l'ottimizzatore produce un piano che utilizza predicati pass-through:

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 CASEespressioni vengono testate in ordine e le sottoquery vengono valutate solo se nessuna espressione precedente è stata soddisfatta.

Espressioni CASE con una sottoquery EXISTS

Laddove viene CASEutilizzata 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 EXISTSseguito è 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 RemoveSubqInPrjin 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:

Semi NLJ Unisciti alla sonda

Il calcolo scalare finale valuta il risultato CASEdell'espressione usando il valore della colonna della sonda:

Calcola l'espressione scalare

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:

Unisci con la colonna della sonda

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 T2corrispondenza corrispondono all'intervallo su ogni iterazione. Con un piano di unione o hash, questa potrebbe essere un'ottimizzazione utile.

Se aggiungiamo un BETWEENpredicato corrispondente a T2nella 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;

Predicato residuo

Spereremmo che il BETWEENpredicato venga invece spinto verso il basso, T2risultando in una ricerca. Normalmente, l'ottimizzatore prenderebbe in considerazione di farlo (anche senza il predicato aggiuntivo nella query). Riconosce i predicati impliciti ( BETWEENon T1e il predicato join tra T1e T2insieme implicano BETWEENon 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;

TOP piano di trucco

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


6

L' argomento "COUNT (*) vs EXISTS" ha a che fare con la verifica dell'esistenza di un record. Per esempio:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

vs

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

Il tuo script SQL non utilizza COUNT(*)come controllo di record esistente e quindi non direi che è applicabile nel tuo scenario.


Qualche pensiero / conclusione basato sullo script che ho pubblicato?
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.