L'indice su colonna calcolata persistente necessita di una ricerca per ottenere colonne nell'espressione calcolata


24

Ho una colonna calcolata persistente su una tabella che è semplicemente composta da colonne concatenate, ad es

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

In questo Comp non è univoco e D è la data di inizio valida di ciascuna combinazione di A, B, C, quindi utilizzo la seguente query per ottenere la data di fine per ciascuna A, B, C(sostanzialmente la data di inizio successiva per lo stesso valore di Comp):

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

Ho quindi aggiunto un indice alla colonna calcolata per aiutare in questa query (e anche in altri):

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

Il piano di query tuttavia mi ha sorpreso. Avrei pensato che da quando ho una clausola where che lo affermaD IS NOT NULL e sto ordinando per Comp, e non facendo riferimento a nessuna colonna esterna all'indice, l'indice sulla colonna calcolata potrebbe essere usato per scansionare t1 e t2, ma ho visto un indice cluster scansione.

inserisci qui la descrizione dell'immagine

Quindi ho costretto l'uso di questo indice per vedere se ha prodotto un piano migliore:

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

Che ha dato questo piano

inserisci qui la descrizione dell'immagine

Ciò dimostra che viene utilizzata una ricerca chiave, i cui dettagli sono:

inserisci qui la descrizione dell'immagine

Ora, secondo la documentazione di SQL Server:

È possibile creare un indice su una colonna calcolata definita con un'espressione deterministica, ma imprecisa, se la colonna è contrassegnata come PERSISTED nell'istruzione CREATE TABLE o ALTER TABLE. Ciò significa che il Motore di database memorizza i valori calcolati nella tabella e li aggiorna quando vengono aggiornate eventuali altre colonne da cui dipende la colonna calcolata. Motore di database utilizza questi valori persistenti quando crea un indice sulla colonna e quando si fa riferimento all'indice in una query. Questa opzione consente di creare un indice su una colonna calcolata quando Motore di database non è in grado di dimostrare con precisione se una funzione che restituisce espressioni di colonne calcolate, in particolare una funzione CLR creata in .NET Framework, sia deterministica e precisa.

Quindi se, come dicono i documenti "Motore di database memorizza i valori calcolati nella tabella" e il valore viene anche memorizzato nel mio indice, perché è richiesta una Ricerca chiave per ottenere A, B e C quando non sono referenziati in la domanda a tutti? Presumo che vengano utilizzati per calcolare Comp, ma perché? Inoltre, perché la query può utilizzare l'indice su t2, ma non su t1?

Query e DDL su SQL Fiddle

NB Ho taggato SQL Server 2008 perché questa è la versione in cui si trova il mio problema principale, ma ho anche lo stesso comportamento nel 2012.

Risposte:


20

Perché è richiesta una Ricerca chiave per ottenere A, B e C quando non sono affatto referenziati nella query? Presumo che vengano utilizzati per calcolare Comp, ma perché?

colonne A, B, and C sono referenziate nel piano di query: sono utilizzate dalla ricerca T2.

Inoltre, perché la query può utilizzare l'indice su t2, ma non su t1?

L'ottimizzatore ha deciso che la scansione dell'indice cluster era più economica della scansione dell'indice filtrato non cluster e quindi di eseguire una ricerca per recuperare i valori per le colonne A, B e C.

Spiegazione

La vera domanda è perché l'ottimizzatore sentiva la necessità di recuperare A, B e C per la ricerca dell'indice. Ci aspetteremmo che leggesse ilComp colonna utilizzando una scansione dell'indice non cluster e quindi esegua una ricerca sullo stesso indice (alias T2) per individuare il primo record.

Query Optimizer espande i riferimenti di colonna calcolati prima dell'inizio dell'ottimizzazione, per consentire di valutare i costi di vari piani di query. Per alcune query, l'espansione della definizione di una colonna calcolata consente all'ottimizzatore di trovare piani più efficienti.

Quando l'ottimizzatore rileva una sottoquery correlata, tenta di "srotolarlo" in un modulo che trova più facile ragionare. Se non riesce a trovare una semplificazione più efficace, ricorre alla riscrittura della sottoquery correlata come applicazione (un join correlato):

Applica riscrittura

Accade solo che questo srotolamento applicativo metta l'albero delle query logiche in una forma che non funziona bene con la normalizzazione del progetto (una fase successiva che cerca di abbinare le espressioni generali alle colonne calcolate, tra le altre cose).

Nel tuo caso, il modo in cui la query viene scritta interagisce con i dettagli interni dell'ottimizzatore in modo tale che la definizione dell'espressione espansa non corrisponda alla colonna calcolata e si finisce con una ricerca che fa riferimento alle colonne A, B, and Canziché alla colonna calcolata,Comp . Questa è la causa principale.

Soluzione

Un'idea per aggirare questo effetto collaterale è scrivere la query come applicare manualmente:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Sfortunatamente, questa query non utilizzerà l'indice filtrato come speriamo. Il test di disuguaglianza sulla colonna Dall'interno dell'applicazione si rifiuta NULLs, quindi il predicato apparentemente ridondante WHERE T1.D IS NOT NULLviene ottimizzato.

Senza tale predicato esplicito, la logica di corrispondenza dell'indice filtrato decide che non può utilizzare l'indice filtrato. Esistono diversi modi per aggirare questo secondo effetto collaterale, ma il più semplice è probabilmente quello di cambiare la croce applicare a un'applicazione esterna (rispecchiando la logica della riscrittura dell'ottimizzatore eseguita in precedenza sulla sottoquery correlata):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Ora l'ottimizzatore non ha bisogno di usare lo stesso apply rewrite (quindi la corrispondenza della colonna calcolata funziona come previsto) e neanche il predicato è ottimizzato, quindi l'indice filtrato può essere usato per entrambe le operazioni di accesso ai dati, e la ricerca usa la Compcolonna su entrambi i lati:

Piano di applicazione esterna

Questo sarebbe generalmente preferito rispetto all'aggiunta di A, B e C come INCLUDEd colonne nell'indice filtrato, poiché risolve la causa principale del problema e non richiede di allargare inutilmente l'indice.

Colonne calcolate persistenti

Come nota a margine, non è necessario contrassegnare la colonna calcolata come PERSISTED, se non ti dispiace ripetere la sua definizione in un CHECKvincolo:

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

La colonna calcolata deve essere PERSISTEDin questo caso solo se si desidera utilizzare un NOT NULLvincolo o fare riferimento Compdirettamente alla colonna (anziché ripetere la sua definizione) in un CHECKvincolo.


2
+1 BTW Mi sono imbattuto in un altro caso di ricerca superflua mentre guardavo questo che potresti (o non potresti) trovare di interesse. SQL Fiddle .
Martin Smith,

@MartinSmith Sì, è interessante. Un'altra regola generica rewrite ( FOJNtoLSJNandLASJN) che fa sì che le cose non funzionino come speriamo e che lasciano spazzatura (BaseRow / Checksums) che è utile in alcuni tipi di piani (es. Cursori) ma non è necessaria qui.
Paul White dice GoFundMonica

Ah Chkè checksum! Grazie non ne ero sicuro. Inizialmente pensavo che potesse essere qualcosa a che fare con i vincoli di controllo.
Martin Smith,

6

Anche se questo potrebbe essere un po 'una coincidenza a causa della natura artificiale dei tuoi dati di test, essendo come hai detto SQL 2012 ho provato a riscrivere:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

Ciò ha prodotto un buon piano a basso costo utilizzando il tuo indice e con letture significativamente inferiori rispetto alle altre opzioni (e gli stessi risultati per i tuoi dati di test).

Pianifica i costi di Explorer per quattro opzioni: Originale;  originale con suggerimento;  esterno applicare e piombo

Sospetto che i tuoi dati reali siano più complicati, quindi potrebbero esserci alcuni scenari in cui questa query si comporta in modo semanticamente diverso dai tuoi, ma a volte mostra che le nuove funzionalità possono fare davvero la differenza.

Ho sperimentato alcuni dati più vari e ho trovato alcuni scenari da abbinare e altri no:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'

1
Bene usa l'indice ma solo fino a un certo punto. Se compnon è una colonna calcolata, non viene visualizzato l'ordinamento.
Martin Smith,

Grazie. Il mio scenario reale non è molto più complicato e la LEADfunzione ha funzionato esattamente come vorrei sulla mia istanza locale di 2012 express. Sfortunatamente, questo piccolo inconveniente per me non è stato ancora considerato un motivo sufficiente per aggiornare i server di produzione ...
GarethD

-1

Quando ho provato a eseguire le stesse azioni, ho ottenuto gli altri risultati. Innanzitutto, il mio piano di esecuzione per la tabella senza indici è il seguente:inserisci qui la descrizione dell'immagine

Come possiamo vedere dalla scansione dell'indice cluster (t2), il predicato viene utilizzato per determinare le righe necessarie da restituire (a causa della condizione):

inserisci qui la descrizione dell'immagine

Quando l'indice è stato aggiunto, non importa se è stato definito dall'operatore WITH o no, il piano di esecuzione è diventato il seguente:

inserisci qui la descrizione dell'immagine

Come possiamo vedere, la scansione dell'indice cluster viene sostituita dalla scansione dell'indice. Come abbiamo visto sopra, SQL Server utilizza le colonne di origine della colonna calcolata per eseguire la corrispondenza della query nidificata. Durante la scansione dell'indice cluster tutti questi valori possono essere acquisiti contemporaneamente (non sono necessarie operazioni aggiuntive). Quando è stato aggiunto l'indice, il filtraggio delle righe necessarie dalla tabella (nella selezione principale) viene eseguito in base all'indice, ma i valori delle colonne di origine per la colonna calcolata compdevono ancora essere ottenuti (ultima operazione Nested Loop) .

inserisci qui la descrizione dell'immagine

Per questo motivo viene utilizzata l'operazione di ricerca chiave - per ottenere i dati delle colonne di origine di quella calcolata.

PS Sembra un bug in SQL Server.

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.