SQL Server 2014: qualche spiegazione per una stima incoerente della cardinalità di autoadesione?


27

Si consideri il seguente piano di query in SQL Server 2014:

inserisci qui la descrizione dell'immagine

Nel piano di query, un auto-join ar.fId = ar.fIdgenera una stima di 1 riga. Tuttavia, questa è una stima logicamente incoerente: arha 20,608righe e solo un valore distinto di fId(riflesso accuratamente nelle statistiche). Pertanto, questo join produce l'intero prodotto incrociato di righe ( ~424MMrighe), causando l'esecuzione della query per diverse ore.

Sto facendo fatica a capire perché SQL Server potrebbe fornire una stima che può essere così facilmente dimostrata incompatibile con le statistiche. Qualche idea?

Indagine iniziale e dettagli aggiuntivi

Sulla base della risposta di Paul qui , sembra che sia l'euristica di SQL 2012 che quella di SQL 2014 per la stima della cardinalità di join dovrebbero gestire facilmente una situazione in cui è necessario confrontare due istogrammi identici.

Ho iniziato con l'output di trace flag 2363, ma non sono riuscito a capirlo facilmente. Il frammento seguente significa che SQL Server sta confrontando gli istogrammi per fIde bIdal fine di stimare la selettività di un join che utilizza solo fId? In tal caso, ciò ovviamente non sarebbe corretto. O sto leggendo male l'output del flag di traccia?

Plan for computation:
  CSelCalcExpressionComparedToExpression( QCOL: [ar].fId x_cmpEq QCOL: [ar].fId )
Loaded histogram for column QCOL: [ar].bId from stats with id 3
Loaded histogram for column QCOL: [ar].fId from stats with id 1
Selectivity: 0

Si noti che ho escogitato diverse soluzioni alternative, che sono incluse nello script repro completo e riducono questa query a millisecondi. Questa domanda è focalizzata sulla comprensione del comportamento, su come evitarlo nelle query future e sulla determinazione se si tratta di un bug che dovrebbe essere archiviato con Microsoft.

Ecco uno script di repro completo , ecco l' output completo dal flag di traccia 2363 , ed ecco le definizioni di query e tabella nel caso in cui desideri guardarle rapidamente senza aprire lo script completo:

WITH cte AS (
    SELECT ar.fId, 
        ar.bId,
        MIN(CONVERT(INT, ar.isT)) AS isT,
        MAX(CONVERT(INT, tcr.isS)) AS isS
    FROM  #SQL2014MinMaxAggregateCardinalityBug_ar ar 
    LEFT OUTER JOIN #SQL2014MinMaxAggregateCardinalityBug_tcr tcr
        ON tcr.rId = 508
        AND tcr.fId = ar.fId
        AND tcr.bId = ar.bId
    GROUP BY ar.fId, ar.bId
)
SELECT s.fId, s.bId, s.isS, t.isS
FROM cte s 
JOIN cte t 
    ON t.fId = s.fId 
    AND t.isT = 1

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_ar (
    fId INT NOT NULL,
    bId INT NOT NULL,
    isT BIT NOT NULL
    PRIMARY KEY (fId, bId)
)

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_tcr (
    rId INT NOT NULL,
    fId INT NOT NULL,
    bId INT NOT NULL,
    isS BIT NOT NULL
    PRIMARY KEY (rId, fId, bId, isS)
)

Risposte:


23

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 = 1spinta sottostante, dove t.isTè MIN(CONVERT(INT, ar.isT)). Nonostante ciò, il calcolo della selettività per il isTpredicato è in grado di utilizzare CSelCalcColumnInIntervalsu 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 CSelCalcExpressionComparedToExpressioncalcolatrice:

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 fIdistogrammi non filtrati , modificati dalla selettività del filtro, che dà 20608 * 20608 * 4.85248e-005 = 20608righe è molto ragionevole.

Seguire questo calcolo significherebbe usare la calcolatrice CSelCalcSimpleJoinWithDistinctCountsinvece di CSelCalcExpressionComparedToExpression. Non esiste un modo documentato per farlo, ma se sei curioso, puoi abilitare il flag di traccia non documentato 9479:

9479 piano

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):

9473 piano

Il comportamento della stima della cardinalità di join con CSelCalcExpressionComparedToExpressionpuò 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 GbAggAfterJoinSelsposta 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.

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.