Sto facendo fatica a capire perché SQL Server potrebbe fornire una stima che può essere così facilmente dimostrata incompatibile con le statistiche.
Consistenza
Non esiste una garanzia generale di coerenza. Le stime possono essere calcolate su sottotitoli diversi (ma logicamente equivalenti) in momenti diversi, utilizzando metodi statistici diversi.
Non c'è nulla di sbagliato nella logica che dice che unire quei due sottotitoli identici dovrebbe produrre un prodotto incrociato, ma non c'è ugualmente nulla da dire che la scelta del ragionamento sia più sana di qualsiasi altra.
Stima iniziale
Nel tuo caso specifico, la stima cardinalità iniziale per il join non viene eseguita su due sottotitoli identici . La forma dell'albero in quel momento è:
LogOp_Join
LogOp_GbAgg
LogOp_LeftOuterJoin
LogOp_Get TBL: ar
LogOp_Select
LogOp_Get TBL: tcr
ScaOp_Comp x_cmpEq
ScaOp_Identifier [tcr] .rId
ScaOp_Const Value = 508
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .fId
ScaOp_Identifier [tcr] .fId
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .bId
ScaOp_Identifier [tcr] .bId
AncOp_PrjList
AncOp_PrjEl Expr1003
ScaOp_AggFunc stopMax
ScaOp_Convert int
ScaOp_Identifier [tcr] .isS
LogOp_Select
LogOp_GbAgg
LogOp_LeftOuterJoin
LogOp_Get TBL: ar
LogOp_Select
LogOp_Get TBL: tcr
ScaOp_Comp x_cmpEq
ScaOp_Identifier [tcr] .rId
ScaOp_Const Value = 508
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .fId
ScaOp_Identifier [tcr] .fId
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .bId
ScaOp_Identifier [tcr] .bId
AncOp_PrjList
AncOp_PrjEl Expr1006
ScaOp_AggFunc stopMin
ScaOp_Convert int
ScaOp_Identifier [ar] .isT
AncOp_PrjEl Expr1007
ScaOp_AggFunc stopMax
ScaOp_Convert int
ScaOp_Identifier [tcr] .isS
ScaOp_Comp x_cmpEq
ScaOp_Identifier Expr1006
ScaOp_Const Value = 1
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [ar] .fId
ScaOp_Identifier QCOL: [ar] .fId
L'ingresso prima aderire ha avuto un aggregato non proiettato semplificata di distanza, e la seconda aderire ingresso ha il predicato t.isT = 1
spinta sottostante, dove t.isT
è MIN(CONVERT(INT, ar.isT))
. Nonostante ciò, il calcolo della selettività per il isT
predicato è in grado di utilizzare CSelCalcColumnInInterval
su un istogramma:
CSelCalcColumnInInterval
Colonna: COL: Expr1006
Istogramma caricato per la colonna QCOL: [ar] .isT dalle statistiche con ID 3
Selettività: 4.85248e-005
Raccolta di statistiche generata:
CStCollFilter (ID = 11, CARD = 1)
CStCollGroupBy (ID = 10, CARD = 20608)
CStCollOuterJoin (ID = 9, CARD = 20608 x_jtLeftOuter)
CStCollBaseTable (ID = 3, CARD = 20608 TBL: ar)
CStCollFilter (ID = 8, CARD = 1)
CStCollBaseTable (ID = 4, CARD = 28 TBL: tcr)
L'aspettativa (corretta) è che 20.608 righe vengano ridotte a 1 riga da questo predicato.
Partecipa alla stima
La domanda ora diventa come le 20.608 righe dell'altro input di join corrisponderanno a questa riga:
LogOp_Join
CStCollGroupBy (ID = 7, CARD = 20608)
CStCollOuterJoin (ID = 6, CARD = 20608 x_jtLeftOuter)
...
CStCollFilter (ID = 11, CARD = 1)
CStCollGroupBy (ID = 10, CARD = 20608)
...
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [ar] .fId
ScaOp_Identifier QCOL: [ar] .fId
Esistono diversi modi per stimare il join in generale. Potremmo, ad esempio:
- Derivare nuovi istogrammi su ciascun operatore del piano in ogni sottostruttura, allinearli al join (interpolando i valori dei passi secondo necessità) e vedere come si combinano; o
- Eseguire un allineamento 'grossolano' più semplice degli istogrammi (utilizzando i valori minimo e massimo, non passo per passo); o
- Calcola selettività separate per le sole colonne di join (dalla tabella di base e senza alcun filtro), quindi aggiungi l'effetto di selettività dei predicati non di join.
- ...
A seconda dello stimatore della cardinalità in uso e di alcune euristiche, è possibile utilizzare uno di questi (o una variante). Per ulteriori informazioni, consultare il White paper di Microsoft Ottimizzazione dei piani di query con lo stimatore di cardinalità di SQL Server 2014 .
Bug?
Ora, come notato nella domanda, in questo caso il join "semplice" a colonna singola (attivo fId
) utilizza la CSelCalcExpressionComparedToExpression
calcolatrice:
Pianificare il calcolo:
CSelCalcExpressionComparedToExpression [ar] .fId x_cmpEq [ar] .fId
Istogramma caricato per la colonna QCOL: [ar] .bId dalle statistiche con ID 2
Istogramma caricato per la colonna QCOL: [ar] .fId dalle statistiche con ID 1
Selettività: 0
Questo calcolo valuta che l'unione delle 20.608 righe con la 1 riga filtrata avrà una selettività zero: nessuna riga corrisponderà (riportata come una riga nei piani finali). È sbagliato? Sì, probabilmente c'è un bug nel nuovo CE qui. Si potrebbe sostenere che 1 riga corrisponderà a tutte le righe o nessuna, quindi il risultato potrebbe essere ragionevole, ma c'è motivo di credere diversamente.
I dettagli sono in realtà piuttosto complicati, ma l'aspettativa per la stima di basarsi su fId
istogrammi non filtrati , modificati dalla selettività del filtro, che dà 20608 * 20608 * 4.85248e-005 = 20608
righe è molto ragionevole.
Seguire questo calcolo significherebbe usare la calcolatrice CSelCalcSimpleJoinWithDistinctCounts
invece di CSelCalcExpressionComparedToExpression
. Non esiste un modo documentato per farlo, ma se sei curioso, puoi abilitare il flag di traccia non documentato 9479:
Nota il join finale produce 20.608 righe da due input a riga singola, ma ciò non dovrebbe sorprendere. È lo stesso piano prodotto dalla CE originale con TF 9481.
Ho detto che i dettagli sono complicati (e richiedono tempo per indagare), ma per quanto ne so, la causa principale del problema è legata al predicato rId = 508
, con una selettività zero. Questa stima zero viene elevata a una riga nel modo normale, il che sembra contribuire alla stima della selettività zero nel join in questione quando tiene conto dei predicati inferiori nella struttura di input (quindi caricamento delle statistiche per bId
).
Consentire al join esterno di mantenere una stima del lato interno della riga zero (invece di aumentare a una riga) (quindi tutte le righe esterne si qualificano) fornisce una stima del join "senza errori" con entrambi i calcolatori. Se ti interessa esplorare questo, il flag di traccia non documentato è 9473 (da solo):
Il comportamento della stima della cardinalità di join con CSelCalcExpressionComparedToExpression
può anche essere modificato per non tenere conto di `` bId` con un altro flag di variazione non documentato (9494). Cito tutte queste cose perché so che hai un interesse per queste cose; non perché offrono una soluzione. Fino a quando non si segnala il problema a Microsoft e non lo risolvono (o meno), esprimere la query in modo diverso è probabilmente la soluzione migliore. Indipendentemente dal fatto che il comportamento sia intenzionale o meno, dovrebbero essere interessati a conoscere la regressione.
Infine, per riordinare un'altra cosa menzionata nello script di riproduzione: la posizione finale del filtro nel piano delle domande è il risultato di un'esplorazione basata sui costi che GbAggAfterJoinSel
sposta l'aggregato e il filtro sopra il join, poiché l'output del join ha un valore così piccolo numero di righe. Il filtro era inizialmente sotto il join, come previsto.