Perché questo CTE ricorsivo con un parametro non utilizza un indice quando lo fa con un valore letterale?


8

Sto usando un CTE ricorsivo su una struttura ad albero per elencare tutti i discendenti di un particolare nodo nella struttura. Se scrivo un valore letterale di nodo nella mia WHEREclausola, SQL Server sembra effettivamente applicare il CTE proprio a quel valore, fornendo un piano di query con conteggi di righe effettivi bassi, eccetera :

piano di query con valore letterale

Tuttavia, se passo il valore come parametro, sembra realizzare (spool) il CTE e quindi filtrarlo dopo il fatto :

piano di query con valore di parametro

Potrei leggere male i piani. Non ho notato un problema di prestazioni, ma sono preoccupato che la realizzazione del CTE possa causare problemi con set di dati più grandi, specialmente in un sistema più occupato. Inoltre, normalmente compongo questo attraversamento su se stesso: attraverso fino agli antenati e torno ai discendenti (per assicurarmi di raccogliere tutti i nodi correlati). A causa di come sono i miei dati, ogni set di nodi "correlati" è piuttosto piccolo, quindi la realizzazione del CTE non ha senso. E quando SQL Server sembra realizzare il CTE, mi sta dando alcuni numeri abbastanza grandi nei suoi conteggi "reali".

C'è un modo per far sì che la versione parametrizzata della query si comporti come la versione letterale? Voglio mettere il CTE in una vista riutilizzabile.

Query con letterale:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Query con parametro:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Codice di installazione:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;

Risposte:


12

La risposta di Randi Vertongen affronta correttamente come ottenere il piano desiderato con la versione con parametri della query. Questa risposta integra che affrontando il titolo della domanda nel caso in cui tu sia interessato ai dettagli.

SQL Server riscrive le espressioni di tabella comuni (CTE) ricorsive della coda come iterazione. Tutto da Lazy Index Spool down è l'implementazione runtime della traduzione iterativa. Ho scritto un resoconto dettagliato di come questa parte di un piano di esecuzione opere in risposta ad usare eccetto in una ricorsiva di tabella comune .

Si desidera specificare un predicato (filtro) all'esterno del CTE e fare in modo che Query Optimizer spinga questo filtro verso il basso all'interno della ricorsione (riscritto come iterazione) e lo si applichi al membro di ancoraggio. Ciò significa che la ricorsione inizia solo con quei record corrispondenti ParentId = @Id.

Questa è un'aspettativa abbastanza ragionevole, sia che si utilizzi un valore letterale, una variabile o un parametro; tuttavia, l'ottimizzatore può fare solo cose per le quali sono state scritte delle regole. Le regole specificano come viene modificato un albero di query logico per ottenere una trasformazione particolare. Includono la logica per assicurarsi che il risultato finale sia sicuro, ovvero che restituisca esattamente gli stessi dati della specifica della query originale in tutti i casi possibili.

Viene chiamata la regola responsabile della spinta dei predicati su un CTE ricorsivo SelOnIterator: una selezione relazionale (= predicato) su un iteratore che implementa la ricorsione. Più precisamente, questa regola può copiare una selezione fino alla parte di ancoraggio dell'iterazione ricorsiva:

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Questa regola può essere disabilitata con il suggerimento non documentato OPTION(QUERYRULEOFF SelOnIterator). Quando viene utilizzato, l'ottimizzatore non può più spingere i predicati con un valore letterale fino all'ancoraggio di un CTE ricorsivo. Non lo vuoi, ma illustra il punto.

Inizialmente, questa regola si limitava a lavorare su predicati con solo valori letterali. Potrebbe anche essere fatto funzionare con variabili o parametri specificando OPTION (RECOMPILE), poiché quel suggerimento abilita l' ottimizzazione dell'incorporamento dei parametri , per cui il valore letterale di runtime della variabile (o parametro) viene utilizzato durante la compilazione del piano. Il piano non è memorizzato nella cache, quindi il rovescio della medaglia è una nuova raccolta su ogni esecuzione.

Ad un certo punto, la SelOnIteratorregola è stata migliorata per funzionare anche con variabili e parametri. Per evitare modifiche impreviste del piano, questo è stato protetto con il flag di traccia 4199, il livello di compatibilità del database e il livello di compatibilità dell'aggiornamento rapido di Query Optimizer. Questo è un modello abbastanza normale per i miglioramenti dell'ottimizzatore, che non sono sempre documentati. I miglioramenti sono normalmente buoni per la maggior parte delle persone, ma c'è sempre la possibilità che qualsiasi cambiamento introduca una regressione per qualcuno.

Voglio mettere il CTE in una vista riutilizzabile

È possibile utilizzare una funzione con valori di tabella incorporata anziché una vista. Fornire il valore che si desidera spingere verso il basso come parametro e posizionare il predicato nel membro di ancoraggio ricorsivo.

Se si preferisce, abilitare l'opzione flag di traccia 4199 a livello globale è anche un'opzione. Esistono molte modifiche all'ottimizzatore coperte da questo flag, quindi è necessario testare attentamente il carico di lavoro con questo abilitato ed essere pronti a gestire le regressioni.


10

Anche se al momento non ho il titolo dell'aggiornamento rapido effettivo, verrà utilizzato il piano di query migliore quando si abilitano gli aggiornamenti rapidi di Query Optimizer nella versione (SQL Server 2012).

Alcuni altri metodi sono:

  • L'utilizzo in OPTION(RECOMPILE)tal modo del filtro avviene in precedenza, sul valore letterale.
  • In SQL Server 2016 o versioni successive gli hotfix precedenti a questa versione vengono applicati automaticamente e la query dovrebbe essere eseguita in modo equivalente al piano di esecuzione migliore.

Aggiornamenti rapidi dell'ottimizzatore di query

È possibile abilitare queste correzioni con

  • Traceflag 4199 prima di SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; a partire da SQL Server 2016. (non necessario per la correzione)

Il filtro attivo @idviene applicato in precedenza a entrambi i membri ricorsivo e di ancoraggio nel piano di esecuzione con l'aggiornamento rapido abilitato.

Il traceflag può essere aggiunto a livello di query:

OPTION(QUERYTRACEON 4199)

Quando si esegue la query su SQL Server 2012 SP4 GDR o SQL Server 2014 SP3 con Traceflag 4199, viene scelto il piano di query migliore:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

Piano di query su SQL Server 2014 SP3 con traceflag 4199

Piano di query su SQL Server 2012 SP4 GDR con traceflag 4199

Piano di query su GDR SQL Server 2012 SP4 senza traceflag 4199

Il consenso principale è abilitare traceflag 4199 a livello globale quando si utilizza una versione precedente a SQL Server 2016. Successivamente è possibile discutere se abilitarlo o meno. AQ / A su questo qui .


Livello di compatibilità 130 o 140

Durante il test della query con parametri su un database con compatibility_level= 130 o 140, il filtro viene eseguito in precedenza:

inserisci qui la descrizione dell'immagine

A causa del fatto che le "vecchie" correzioni di traceflag 4199 sono abilitate su SQL Server 2016 e versioni successive.


OPTION (RECOMPILE)

Anche se viene utilizzata una procedura, SQL Server sarà in grado di filtrare il valore letterale durante l'aggiunta OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

inserisci qui la descrizione dell'immagine

Piano di query su GDR SQL Server 2012 SP4 con OPTION (RECOMPILE)

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.