Perché una sottoquery riduce la stima delle righe a 1?


26

Considera la seguente query semplice ma inventata:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP;

Mi aspetto che la stima della riga finale per questa query sia uguale al numero di righe nella X_HEAPtabella. Qualunque cosa sto facendo nella sottoquery non dovrebbe importare per la stima delle righe perché non può filtrare nessuna riga. Tuttavia, su SQL Server 2016 vedo la stima delle righe ridotta a 1 a causa della sottoquery:

query errata

Perché succede? Cosa posso fare al riguardo?

È molto facile riprodurre questo problema con la sintassi corretta. Ecco un insieme di definizioni di tabella che lo farà:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL)
CREATE TABLE dbo.X_OTHER_TABLE (ID INT NOT NULL);
CREATE TABLE dbo.X_OTHER_TABLE_2 (ID INT NOT NULL);

INSERT INTO dbo.X_HEAP WITH (TABLOCK)
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values;

CREATE STATISTICS X_HEAP__ID ON X_HEAP (ID) WITH FULLSCAN;

db fiddle link .

Risposte:


22

Questo problema di stima della cardinalità (CE) emerge quando:

  1. Il join è un join esterno con un predicato pass-through
  2. Si stima che la selettività del predicato pass-through sia esattamente 1 .

Nota: il calcolatore particolare utilizzato per determinare la selettività non è importante.


Dettagli

Il CE calcola la selettività del join esterno come la somma di:

  • La selettività del join interno con lo stesso predicato
  • La selettività anti join con lo stesso predicato

L'unica differenza tra un join esterno e interno è che un join esterno restituisce anche righe che non corrispondono sul predicato del join. L'anti join fornisce esattamente questa differenza. La stima della cardinalità per il join interno e anti join è più semplice rispetto al join esterno direttamente.

Il processo di stima della selettività del join è molto semplice:

  • Innanzitutto, viene valutata la selettività del predicato pass-through. SPT
    • Questo viene fatto usando qualunque calcolatore sia appropriato alle circostanze.
    • Il predicato è il tutto, incluso qualsiasi IsFalseOrNullcomponente negativo .
  • Selettività del join interno: = 1 - SPT
  • Selettività anti join: = SPT

L'anti join rappresenta le righe che "passeranno" attraverso l'unione. Il join interno rappresenta le righe che non "passeranno attraverso". Si noti che "pass through" indica le righe che scorrono attraverso il join senza eseguire affatto il lato interno. Per sottolineare: tutte le righe verranno restituite dal join, la distinzione è tra le righe che eseguono il lato interno del join prima di emergere e quelle che non lo fanno.

Chiaramente, l'aggiunta a dovrebbe sempre dare una selettività totale di 1, il che significa che tutte le righe vengono restituite dal join, come previsto.1 - SPTSPT

In effetti, il calcolo di cui sopra funziona esattamente come descritto per tutti i valori tranne 1 .SPT

Quando = 1, le selettività di join interno e anti-join sono stimate a zero, risultando in una stima di cardinalità (per l'unione nel suo insieme) di una riga. Per quanto ne so, questo non è intenzionale e dovrebbe essere segnalato come un bug.SPT


Un problema correlato

È più probabile che questo errore si manifesti di quanto si pensi, a causa di una limitazione CE separata. Ciò si verifica quando l' CASEespressione utilizza una EXISTSclausola (come è comune). Ad esempio, la seguente query modificata dalla domanda non rileva la stima di cardinalità imprevista:

-- This is fine
SELECT 
    CASE
        WHEN XH.ID = 1
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

L'introduzione di un banale EXISTSfa emergere il problema:

-- This is not fine
SELECT 
    CASE
        WHEN EXISTS (SELECT 1 WHERE XH.ID = 1)
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

L'utilizzo EXISTSintroduce un semi join (evidenziato) al piano di esecuzione:

Piano semi join

La stima per il semi join va bene. Il problema è che la CE tratta la colonna sonda associata come una semplice proiezione, con una selettività fissa di 1:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

Ciò soddisfa automaticamente una delle condizioni richieste per manifestare questo problema CE, indipendentemente dal contenuto della EXISTSclausola.


Per informazioni di base importanti, vedere Sottoquery in CASEExpressions di Craig Freedman.


22

Questo sembra sicuramente un comportamento indesiderato. È vero che le stime di cardinalità non devono necessariamente essere coerenti in ogni fase di un piano, ma si tratta di un piano di query relativamente semplice e la stima di cardinalità finale non è coerente con ciò che sta facendo la query. Una stima di cardinalità così bassa potrebbe comportare scelte inadeguate per i tipi di join e metodi di accesso per altre tabelle a valle in un piano più complicato.

Attraverso tentativi ed errori possiamo formulare alcune domande simili per le quali il problema non appare:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Possiamo anche fornire altre domande per le quali appare il problema:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Sembra esserci un modello: se all'interno di un'espressione CASEnon è prevista l'esecuzione e l'espressione del risultato è una sottoquery rispetto a una tabella, la stima della riga scende a 1 dopo tale espressione.

Se scrivo la query su una tabella con un indice cluster, le regole cambiano in qualche modo. Possiamo usare gli stessi dati:

CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))

INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;

UPDATE STATISTICS X_CI WITH FULLSCAN;

Questa query ha una stima finale di 1000 righe:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI;

Ma questa query ha una stima finale di 1 riga:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI;

Per approfondire ulteriormente, possiamo usare il flag di traccia non documentato 2363 per ottenere informazioni su come l'ottimizzatore di query ha eseguito calcoli di selettività. Ho trovato utile associare quel flag di traccia al flag di traccia non documentato 8606 . TF 2363 sembra fornire calcoli di selettività sia per l'albero semplificato che per l'albero dopo la normalizzazione del progetto. L'attivazione di entrambi i flag di traccia chiarisce quali calcoli si applicano a quale albero.

Proviamo per la query originale pubblicata nella domanda:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Ecco parte della parte dell'output che ritengo rilevante insieme ad alcuni commenti:

Plan for computation:

  CSelCalcColumnInInterval -- this is the type of calculator used

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation

Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000

      CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

      CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join

          CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)

Ora proviamo per una query simile che non presenta il problema. Ho intenzione di usare questo:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Uscita di debug alla fine:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table

Proviamo un'altra query per la quale è presente la stima della riga errata:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Alla fine la stima della cardinalità scende a 1 riga, sempre dopo Selettività pass-through = 1. La stima della cardinalità viene conservata dopo una selettività di 0,501 e 0,499.

Plan for computation:

 CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.501

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression

      CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)

          CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)

              CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

              CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

          CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)

Passiamo di nuovo a un'altra query simile che non presenta il problema. Ho intenzione di usare questo:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Nell'output di debug non c'è mai un passaggio che ha una selettività pass-through di 1. La stima della cardinalità rimane a 1000 righe.

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

End selectivity computation

Che dire della query quando coinvolge una tabella con un indice cluster? Considera la seguente query con il problema della stima delle righe:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

La fine dell'output di debug è simile a ciò che abbiamo già visto:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_CI].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

Tuttavia, la query sull'elemento della configurazione senza il problema ha output diversi. Utilizzando questa query:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Risultati in diversi calcolatori utilizzati. CSelCalcColumnInIntervalnon appare più:

Plan for computation:

  CSelCalcFixedFilter (0.559)

Pass-through selectivity: 0.559

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

      CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

      CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

...

Plan for computation:

  CSelCalcUniqueKeyFilter

Pass-through selectivity: 0.001

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)

In conclusione, sembra che si ottenga una stima della riga errata dopo la query secondaria nelle seguenti condizioni:

  1. Il CSelCalcColumnInIntervalcalcolatore di selettività viene utilizzato. Non so esattamente quando viene utilizzato, ma sembra comparire molto più spesso quando la tabella di base è un heap.

  2. Selettività pass-through = 1. In altre parole, CASEsi prevede che una delle espressioni sia valutata come falsa per tutte le righe. Non importa se la prima CASEespressione viene valutata vera per tutte le righe.

  3. C'è un join esterno a CStCollBaseTable. In altre parole, l' CASEespressione del risultato è una sottoquery rispetto a una tabella. Un valore costante non funzionerà.

Forse in queste condizioni l'ottimizzatore di query applica involontariamente la selettività pass-through alla stima di riga della tabella esterna anziché al lavoro svolto sulla parte interna del ciclo nidificato. Ciò ridurrebbe la stima delle righe a 1.

Sono stato in grado di trovare due soluzioni alternative. Non sono stato in grado di riprodurre il problema quando si utilizzava APPLYinvece di una sottoquery. L'output di trace flag 2363 era molto diverso APPLY. Ecco un modo per riscrivere la query originale nella domanda:

SELECT 
  h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END
) a(ID2);

buona query 1

Anche il CE legacy sembra evitare il problema.

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

buona query 2

È stato inviato un elemento di connessione per questo problema (con alcuni dettagli forniti da Paul White nella sua risposta).

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.