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 PLAN
suggerimento - 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 table1
piano 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.