Colonna calcolata persistente che causa scansione


9

La conversione di una colonna normale in una colonna calcolata persistente sta impedendo a questa query di eseguire ricerche di indice. Perché?

Testato su diverse versioni di SQL Server, tra cui SP1 CU1 2016.

repros

Il problema è con table1, col7.

Le tabelle e le query sono una versione parziale (e semplificata) degli originali. Sono consapevole che la query potrebbe essere riscritta in modo diverso, e per qualche ragione evita il problema, ma dobbiamo evitare di toccare il codice e la domanda sul perché table1non può essere cercato rimane valida.

Come ha mostrato Paul White (grazie!), La ricerca è disponibile se forzata, quindi la domanda è: perché la ricerca non è scelta dall'ottimizzatore e se possiamo fare qualcosa di diverso per far sì che la ricerca avvenga come dovrebbe, senza cambiare il codice?

Per chiarire la parte problematica, ecco la scansione rilevante nel piano di cattiva esecuzione:

Piano

Risposte:


12

Perché la ricerca non è stata scelta dall'ottimizzatore


TL: DR La definizione di colonna calcolata espansa interferisce con la capacità dell'ottimizzatore di riordinare i join inizialmente. Con un diverso punto di partenza, l'ottimizzazione basata sui costi segue un percorso diverso attraverso l'ottimizzatore e termina con una scelta del piano finale diversa.


Dettagli

Per tutte le query tranne la più semplice, l'ottimizzatore non tenta di esplorare qualcosa come l'intero spazio dei possibili piani. Invece, sceglie un punto di partenza dall'aspetto ragionevole , quindi spende una quantità di sforzo in bilancio esplorando le variazioni logiche e fisiche, in una o più fasi di ricerca, fino a quando non trova un piano ragionevole.

Il motivo principale per cui si ottengono piani diversi (con diverse stime dei costi finali) per i due casi è che esistono diversi punti di partenza. A partire da un posto diverso, l'ottimizzazione finisce in un posto diverso (dopo il numero limitato di iterazioni di esplorazione e implementazione). Spero che questo sia ragionevolmente intuitivo.

Il punto di partenza che ho citato è in qualche modo basato sulla rappresentazione testuale della query, ma vengono apportate modifiche alla rappresentazione ad albero interna mentre passa attraverso le fasi di analisi, associazione, normalizzazione e semplificazione della compilazione della query.

È importante sottolineare che il punto di partenza esatto dipende fortemente dall'ordine di join iniziale selezionato dall'ottimizzatore. Questa scelta viene effettuata prima che vengano caricate le statistiche e prima che siano state derivate eventuali stime di cardinalità. La cardinalità totale (numero di righe) in ogni tabella è comunque nota, essendo stata ottenuta dai metadati di sistema.

L'ordinamento iniziale dei join si basa quindi sull'euristica . Ad esempio, l'ottimizzatore tenta di riscrivere l'albero in modo tale che le tabelle più piccole vengano unite prima di quelle più grandi e i join interni vengano prima dei join esterni (e dei cross join).

La presenza della colonna calcolata interferisce con questo processo, in particolare con la capacità dell'ottimizzatore di spingere i join esterni lungo l'albero delle query. Questo perché la colonna calcolata viene espansa nella sua espressione sottostante prima che si verifichi il riordino dei join e spostare un join oltre un'espressione complessa è molto più difficile che spostarlo oltre un semplice riferimento di colonna.

Gli alberi coinvolti sono piuttosto grandi, ma per illustrare, l' albero di query iniziale della colonna non calcolata inizia con: (notare i due join esterni in alto)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter) 
        LogOp_LeftOuterJoin
            LogOp_NAryJoin
                LogOp_LeftAntiSemiJoin
                    LogOp_NAryJoin
                        LogOp_Get TBL: dbo.table1 (alias TBL: a4)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table6 (alias TBL: a3)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a3] .col18
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table1 (alias TBL: a1)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a1] .col2
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table5 (alias TBL: a2)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a2] .col2
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a4] .col2
                            ScaOp_Identifier QCOL: [a3] .col19
                    LogOp_Select
                        LogOp_Get TBL: dbo.table7 (alias TBL: a7)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a7] .col22
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a7] .col23
                LogOp_Select
                    LogOp_Get TBL: table1 (alias TBL: cdc)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdc] .col6
                        ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Not Owned, Value = 4)
                LogOp_Get TBL: dbo.table5 (alias TBL: a5) 
                LogOp_Get TBL: table2 (alias TBL: cdt)  
                ScaOp_Logical x_lopAnd
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a5] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdt] .col1
                        ScaOp_Identifier QCOL: [cdc] .col1
            LogOp_Get TBL: table3 (alias TBL: ahcr)
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [ahcr] .col9
                ScaOp_Identifier QCOL: [cdt] .col1

Lo stesso frammento della query di colonna calcolata è: (notare l'unione esterna molto più in basso, la definizione di colonna calcolata espansa e alcune altre sottili differenze nell'ordine (interno) di join)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter)
        LogOp_NAryJoin
            LogOp_LeftAntiSemiJoin
                LogOp_NAryJoin
                    LogOp_Get TBL: dbo.table1 (alias TBL: a4)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table6 (alias TBL: a3)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a3] .col18
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table1 (alias TBL: a1
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a1] .col2
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table5 (alias TBL: a2)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a2] .col2
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a3] .col19
                LogOp_Select
                    LogOp_Get TBL: dbo.table7 (alias TBL: a7) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a7] .col22
                        ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [a7] .col23
            LogOp_Project
                LogOp_LeftOuterJoin
                    LogOp_Join
                        LogOp_Select
                            LogOp_Get TBL: table1 (alias TBL: cdc) 
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [cdc] .col6
                                ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Not Owned, Value = 4)
                        LogOp_Get TBL: table2 (alias TBL: cdt) 
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [cdc] .col1
                            ScaOp_Identifier QCOL: [cdt] .col1
                    LogOp_Get TBL: table3 (alias TBL: ahcr) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [ahcr] .col9
                        ScaOp_Identifier QCOL: [cdt] .col1
                AncOp_PrjList 
                    AncOp_PrjEl QCOL: [cdc] .col7
                        ScaOp_Convert char collate 53256, Null, Trim, ML = 6
                            ScaOp_IIF varchar collate 53256, Null, Var, Trim, ML = 6
                                ScaOp_Comp x_cmpEq
                                    ScaOp_Intrinsic isnumeric
                                        ScaOp_Intrinsic right
                                            ScaOp_Identifier QCOL: [cdc] .col4
                                            ScaOp_Const TI (int, ML = 4) XVAR (int, Not Owned, Value = 4)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Non di proprietà, Valore = 0)
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 1) XVAR (varchar, Owned, Value = Len, Data = (0,))
                                ScaOp_Intrinsic sottostringa
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Non di proprietà, Valore = 6)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Not Owned, Value = 1)
                                    ScaOp_Identifier QCOL: [cdc] .col4
            LogOp_Get TBL: dbo.table5 (alias TBL: a5)
            ScaOp_Logical x_lopAnd
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a5] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2

Le statistiche vengono caricate e una stima iniziale di cardinalità viene eseguita sull'albero subito dopo aver impostato l'ordine di join iniziale. Anche avere join in ordini diversi influisce su queste stime, e quindi ha un effetto a catena durante l'ottimizzazione successiva basata sui costi.

Infine, per questa sezione, avere un join esterno bloccato nel mezzo dell'albero può impedire ulteriori corrispondenze delle regole di riordino dei join durante l'ottimizzazione basata sui costi.


L'uso di una guida di piano (o, equivalentemente, un USE PLANsuggerimento - esempio per la tua query ) modifica la strategia di ricerca in un approccio più orientato agli obiettivi, guidato dalla forma generale e dalle caratteristiche del modello fornito. Questo spiega perché l'ottimizzatore può trovare lo stesso table1piano di ricerca rispetto agli schemi di colonna calcolati e non calcolati, quando viene utilizzata una guida di piano o un suggerimento.

Se possiamo fare qualcosa di diverso per realizzare la ricerca

Questo è qualcosa di cui devi preoccuparti solo se l'ottimizzatore non trova un piano con caratteristiche di prestazione accettabili da solo.

Tutti i normali strumenti di ottimizzazione sono potenzialmente applicabili. Ad esempio, è possibile suddividere la query in parti più semplici, rivedere e migliorare l'indicizzazione disponibile, aggiornare o creare nuove statistiche ... e così via.

Tutte queste cose possono influenzare le stime della cardinalità, il percorso del codice adottato attraverso l'ottimizzatore e influenzare le decisioni basate sui costi in modo sottile.

Alla fine potresti ricorrere all'uso di suggerimenti (o di una guida di piano), ma di solito non è la soluzione ideale.


Domande aggiuntive dai commenti

Sono d'accordo che è meglio semplificare la query, ecc., Ma esiste un modo (flag di traccia) per far continuare l'ottimizzatore con l'ottimizzazione e raggiungere lo stesso risultato?

No, non esiste un flag di traccia per eseguire una ricerca esaustiva e non ne vuoi una. Il possibile spazio di ricerca è vasto e i tempi di compilazione che superano l'età dell'universo non sarebbero ben accolti. Inoltre, l'ottimizzatore non conosce ogni possibile trasformazione logica (nessuno lo sa).

Inoltre, perché è necessaria l'espansione complessa, poiché la colonna è persistente? Perché l'ottimizzatore non può evitare di espanderlo, trattarlo come una colonna normale e raggiungere lo stesso punto di partenza?

Le colonne calcolate vengono espanse (come le viste) per consentire ulteriori opportunità di ottimizzazione. L'espansione può essere adattata ad esempio a una colonna o un indice persistenti più avanti nel processo, ma ciò accade dopo che l' ordine di join iniziale è stato corretto.

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.