La query 100 volte più lenta in SQL Server 2014, la riga Spool conteggio righe stima il colpevole?


13

Ho una query che funziona in 800 millisecondi in SQL Server 2012 e impiega circa 170 secondi in SQL Server 2014 . Penso di averlo ridotto a una stima di cardinalità scadente per l' Row Count Spooloperatore. Ho letto un po 'di operatori di spool (es. Qui e qui ), ma ho ancora problemi a capire alcune cose:

  • Perché questa query richiede un Row Count Spooloperatore? Non credo sia necessario per la correttezza, quindi quale ottimizzazione specifica sta cercando di fornire?
  • Perché SQL Server stima che il join Row Count Spoolall'operatore rimuova tutte le righe?
  • È un bug in SQL Server 2014? In tal caso, invierò il file Connect. Ma prima vorrei una comprensione più profonda.

Nota: posso riscrivere la query come LEFT JOINo aggiungere indici alle tabelle al fine di ottenere prestazioni accettabili sia in SQL Server 2012 che in SQL Server 2014. Quindi questa domanda riguarda più la comprensione di questa specifica query e questo piano in modo approfondito e meno come pronunciare la query in modo diverso.


La query lenta

Vedi questo Pastebin per uno script di test completo. Ecco la query di test specifica che sto guardando:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014: il piano di query stimato

SQL Server ritiene che il Left Anti Semi Jointo Row Count Spoolfiltra le 10.000 righe fino a 1 riga. Per questo motivo, seleziona a LOOP JOINper il successivo join a #existingCustomers.

inserisci qui la descrizione dell'immagine


SQL Server 2014: il piano di query effettivo

Come previsto (da tutti tranne SQL Server!), Row Count SpoolNon è stata rimossa alcuna riga. Quindi eseguiamo il looping 10.000 volte quando SQL Server prevede di eseguire il loop solo una volta.

inserisci qui la descrizione dell'immagine


SQL Server 2012: il piano di query stimato

Quando si utilizza SQL Server 2012 (o OPTION (QUERYTRACEON 9481)in SQL Server 2014), Row Count Spoolnon riduce il numero stimato di righe e viene scelto un join hash, risultando in un piano molto migliore.

inserisci qui la descrizione dell'immagine

La LEFT JOIN riscrive

Per riferimento, ecco un modo in cui posso riscrivere la query per ottenere buone prestazioni in tutti i SQL Server 2012, 2014 e 2016. Tuttavia, sono ancora interessato al comportamento specifico della query sopra e se è un bug nel nuovo stimatore della cardinalità di SQL Server 2014.

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

inserisci qui la descrizione dell'immagine

Risposte:


10

Perché questa query richiede un operatore di spool di conteggio righe? ... quale ottimizzazione specifica sta cercando di fornire?

La cust_nbrcolonna in #existingCustomersè nullable. Se in realtà contiene degli null, la risposta corretta qui è di restituire zero righe ( NOT IN (NULL,...) produrrà sempre un set di risultati vuoto).

Quindi la query può essere considerata come

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

Con la spola rowcount lì per evitare di dover valutare

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

Più di una volta.

Questo sembra essere solo un caso in cui una piccola differenza nelle ipotesi può fare una differenza piuttosto catastrofica nelle prestazioni.

Dopo aver aggiornato una singola riga come di seguito ...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

... la query è stata completata in meno di un secondo. I conteggi delle righe nelle versioni effettive e stimate del piano ora sono quasi esatti.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

inserisci qui la descrizione dell'immagine

Le righe zero vengono emesse come descritto sopra.

Gli istogrammi delle statistiche e le soglie di aggiornamento automatico in SQL Server non sono sufficientemente granulari per rilevare questo tipo di modifica a riga singola. Probabilmente se la colonna è nullable potrebbe essere ragionevole lavorare sulla base del fatto che ne contiene almeno uno NULLanche se l'istogramma delle statistiche non indica attualmente che ce ne siano.


9

Perché questa query richiede un operatore di spool di conteggio righe? Non credo sia necessario per la correttezza, quindi quale ottimizzazione specifica sta cercando di fornire?

Vedi la risposta esauriente di Martin per questa domanda. Il punto chiave è che se una singola riga all'interno di NOT INis NULL, la logica booleana funziona in modo tale che "la risposta corretta è restituire zero righe". L' Row Count Spooloperatore sta ottimizzando questa logica (necessaria).

Perché SQL Server stima che il join all'operatore Spool conteggio righe rimuove tutte le righe?

Microsoft fornisce un eccellente white paper sullo stimatore della cardinalità di SQL 2014 . In questo documento, ho trovato le seguenti informazioni:

Il nuovo CE presuppone che i valori interrogati esistano nel set di dati anche se il valore non rientra nell'intervallo dell'istogramma. La nuova CE in questo esempio utilizza una frequenza media calcolata moltiplicando la cardinalità della tabella per la densità.

Spesso un tale cambiamento è molto buono; allevia notevolmente il problema chiave crescente e in genere produce un piano di query più prudente (stima di riga più elevata) per valori che sono fuori intervallo in base all'istogramma delle statistiche.

Tuttavia, in questo caso specifico, supponendo che NULLverrà trovato un valore, si presume che l'unione a Row Count Spoolfiltrerà tutte le righe da #potentialNewCustomers. Nel caso in cui vi sia effettivamente una NULLriga, questa è una stima corretta (come si vede nella risposta di Martin). Tuttavia, nel caso in cui non ci sia una NULLriga, l'effetto può essere devastante poiché SQL Server produce una stima post-join di 1 riga indipendentemente dal numero di righe di input visualizzate. Ciò può comportare scelte di join molto scarse nel resto del piano di query.

È un bug in SQL 2014? In tal caso, invierò il file Connect. Ma prima vorrei una comprensione più profonda.

Penso che sia nell'area grigia tra un bug e un'ipotesi o limitazione che influisce sulle prestazioni del nuovo Estimatore di cardinalità di SQL Server. Tuttavia, questa stranezza può causare regressioni sostanziali nelle prestazioni rispetto a SQL 2012 nel caso specifico di una NOT INclausola nullable che non ha alcun NULLvalore.

Pertanto, ho presentato un problema di Connect in modo che il team SQL sia a conoscenza delle potenziali implicazioni di questa modifica allo stimatore della cardinalità.

Aggiornamento: Siamo su CTP3 ora per SQL16 e ho confermato che il problema non si verifica lì.


5

La risposta di Martin Smith e la tua auto-risposta hanno affrontato correttamente tutti i punti principali, voglio solo sottolineare un'area per i futuri lettori:

Quindi questa domanda è più sulla comprensione di questa specifica query e piano in profondità e meno su come formulare la query in modo diverso.

Lo scopo dichiarato della query è:

-- Prune any existing customers from the set of potential new customers

Questo requisito è facile da esprimere in SQL, in diversi modi. Quale è scelto è una questione di stile tanto quanto qualsiasi altra cosa, ma le specifiche della query devono essere comunque scritte per restituire risultati corretti in tutti i casi. Ciò include la contabilizzazione di null.

Esprimere pienamente il requisito logico:

  • Restituisci potenziali clienti che non sono già clienti
  • Elencare ogni potenziale cliente al massimo una volta
  • Escludere i clienti nulli potenziali ed esistenti (qualunque cosa significhi un cliente null)

Possiamo quindi scrivere una query corrispondente a tali requisiti utilizzando la sintassi che preferiamo. Per esempio:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Ciò produce un piano di esecuzione efficiente, che restituisce risultati corretti:

Progetto esecutivo

Possiamo esprimere il NOT INcome <> ALLo NOT = ANYsenza influire sul piano o sui risultati:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

O usando NOT EXISTS:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

Non c'è nulla di magico in questo, o qualcosa di particolarmente discutibile sull'uso IN, ANYoppure ALL- dobbiamo solo scrivere correttamente la query, in modo da produrre sempre i risultati giusti.

La forma più compatta utilizza EXCEPT:

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

Questo produce anche risultati corretti, sebbene il piano di esecuzione possa essere meno efficiente a causa dell'assenza di filtri bitmap:

Piano di esecuzione non bitmap

La domanda originale è interessante perché espone un problema che influisce sulle prestazioni con l'implementazione del controllo null necessaria. Il punto di questa risposta è che scrivere correttamente la query evita anche il problema.

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.