Dlaczego podzapytanie zmniejsza oszacowanie wiersza do 1?

26

Rozważ następujące wymyślone, ale proste zapytanie:

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;

Oczekiwałbym, że ostateczny szacunek wiersza dla tego zapytania będzie równy liczbie wierszy w X_HEAPtabeli. Cokolwiek robię w podzapytaniu, nie powinno mieć znaczenia dla oszacowania wiersza, ponieważ nie może odfiltrować żadnych wierszy. Jednak w SQL Server 2016 widzę oszacowanie wiersza zmniejszone do 1 z powodu podzapytania:

złe zapytanie

Dlaczego to się dzieje? Co mogę z tym zrobić?

Bardzo łatwo jest odtworzyć ten problem z właściwą składnią. Oto jeden zestaw definicji tabel, które to zrobią:

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 skrzypce link .

Joe Obbish
źródło

Odpowiedzi:

22

To oszacowanie liczności (CE) wydaje powierzchnie, gdy:

  1. Sprzężenie jest złączeniem zewnętrznym z predykatem przejścia
  2. Selektywność orzecznika pass-through szacuje się dokładnie 1 .

Uwaga: konkretny kalkulator używany do określenia selektywności nie jest ważny.


Detale

CE oblicza selektywność połączenia zewnętrznego jako sumę :

  • Sprzężenia wewnętrznego selektywność samego predykatu
  • Do zwalczania przyłączenia selektywność samego predykatu

Jedyną różnicą między złączem zewnętrznym i wewnętrznym jest to, że łączenie zewnętrzne zwraca również wiersze, które nie pasują do predykatu łączenia. Anty łączyć zapewnia dokładnie tę różnicę. Oszacowanie liczności dla złączenia wewnętrznego i łączącego jest łatwiejsze niż łączenie zewnętrzne bezpośrednio.

Proces oceny selektywności łączenia jest bardzo prosty:

  • Najpierw ocenia się selektywność predykatu tranzytowego. SPT
    • Odbywa się to za pomocą dowolnego kalkulatora odpowiedniego do okoliczności.
    • Predykat to całość, w tym każdy IsFalseOrNullelement negujący .
  • Selektywność łączenia wewnętrznego: = 1 - SPT
  • Selektywność anty łączenia: = SPT

Łączenie anty reprezentuje wiersze, które „przejdą” łączenie. Połączenie wewnętrzne reprezentuje rzędy, które nie przejdą. Zauważ, że „przejście” oznacza rzędy, które przepływają przez złączenie bez uruchamiania wewnętrznej strony. Aby podkreślić: wszystkie wiersze zostaną zwrócone przez złączenie, rozróżnia się między wierszami, które biegną wewnętrzną stroną złączenia przed pojawieniem się, a tymi, które tego nie robią.

Oczywiście dodanie do powinno zawsze dawać całkowitą selektywność 1, co oznacza, że ​​wszystkie wiersze są zwracane przez sprzężenie, zgodnie z oczekiwaniami.1 - SPTSPT

Rzeczywiście powyższe obliczenia działają dokładnie tak, jak opisano dla wszystkich wartości z wyjątkiem 1 .SPT

Gdy = 1, zarówno wybiórczość łączenia wewnętrznego, jak i anty łączenia są szacowane na zero, co daje oszacowanie liczności (dla łączenia jako całości) jednego rzędu. O ile mi wiadomo, jest to niezamierzone i powinno zostać zgłoszone jako błąd.SPT


Powiązany problem

Ten błąd jest bardziej prawdopodobny, niż mogłoby się wydawać, z powodu osobnego ograniczenia CE. Powstaje, gdy CASEwyrażenie używa EXISTSklauzuli (co jest powszechne). Na przykład następujące zmodyfikowane zapytanie z pytania nie napotyka nieoczekiwanego oszacowania liczności:

-- 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;

Wprowadzenie trywialne EXISTSpowoduje, że problem pojawia się na powierzchni:

-- 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;

Użycie EXISTSwprowadza częściowo dołączone (wyróżnione) do planu wykonania:

Częściowo dołącz do planu

Szacunek dla częściowego złączenia jest w porządku. Problem polega na tym, że CE traktuje powiązaną kolumnę sondy jako prostą projekcję ze stałą selektywnością 1:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

To automatycznie spełnia jeden z warunków wymaganych do ujawnienia się tego wydania CE, niezależnie od treści EXISTSklauzuli.


Aby uzyskać ważne informacje dodatkowe, zobacz Podzapytania w CASEwyrażeniach Craiga Freedmana.

Paul White mówi GoFundMonica
źródło
22

To zdecydowanie wydaje się niezamierzonym zachowaniem. Prawdą jest, że szacunki liczności nie muszą być spójne na każdym etapie planu, ale jest to stosunkowo prosty plan zapytań, a ostateczna ocena liczności jest niezgodna z tym, co robi zapytanie. Tak niska ocena liczności może skutkować złymi wyborami typów łączenia i metod dostępu do innych tabel poniżej w bardziej skomplikowanym planie.

Dzięki próbom i błędom możemy zaproponować kilka podobnych zapytań, dla których problem nie pojawia się:

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;

Możemy również zaproponować więcej zapytań, w przypadku których pojawia się problem:

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;

Wygląda na to, że istnieje wzorzec: jeśli w wyrażeniu istnieje wyrażenie, CASEktóre nie powinno zostać wykonane, a wyrażenie wynikowe jest podzapytaniem o tabelę, wówczas oszacowanie wiersza spada do 1 po tym wyrażeniu.

Jeśli napiszę zapytanie do tabeli z indeksem klastrowym, reguły nieco się zmienią. Możemy korzystać z tych samych danych:

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;

To zapytanie ma końcowy szacunek na 1000 wierszy:

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;

Ale to zapytanie ma szacunkową ocenę 1-wierszową:

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;

Aby zagłębić się w tę kwestię, możemy użyć nieudokumentowanej flagi śledzenia 2363, aby uzyskać informacje o tym, jak optymalizator kwerend przeprowadził obliczenia selektywności. Przydało mi się sparowanie tej flagi śledzenia z nieudokumentowaną flagą śledzenia 8606 . TF 2363 wydaje się dawać obliczenia selektywności zarówno dla drzewa uproszczonego, jak i drzewa po normalizacji projektu. Włączenie obu flag śledzenia jasno wskazuje, które obliczenia dotyczą danego drzewa.

Wypróbujmy to dla pierwotnego zapytania zamieszczonego w pytaniu:

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

Oto część tej części, która moim zdaniem jest istotna wraz z kilkoma komentarzami:

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)

Teraz wypróbujmy to dla podobnego zapytania, które nie ma problemu. Użyję tego:

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

Wyjście debugowania na samym końcu:

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

Spróbujmy innego zapytania, dla którego występuje błędny szacunek wiersza:

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

Na samym końcu szacunek liczności spada do 1 rzędu, ponownie po selektywności przejścia = 1. Szacunek liczności jest zachowywany po selektywności 0,501 i 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)

Przejdźmy ponownie do innego podobnego zapytania, które nie ma problemu. Użyję tego:

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

W danych wyjściowych debugowania nigdy nie ma kroku, który miałby selektywność tranzytową równą 1. Szacowana liczność pozostaje na 1000 wierszy.

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

Co z zapytaniem, gdy dotyczy ono tabeli z indeksem klastrowym? Rozważ następujące zapytanie dotyczące problemu z oszacowaniem wiersza:

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

Koniec danych debugowania jest podobny do tego, co już widzieliśmy:

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)

Jednak zapytanie dotyczące CI bez problemu ma inne wyniki. Za pomocą tego zapytania:

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

Wyniki w różnych kalkulatorach. CSelCalcColumnInIntervalnie pojawia się już:

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)

Podsumowując, wydaje się, że po podzapytaniu otrzymujemy błędne oszacowanie wiersza, pod następującymi warunkami:

  1. CSelCalcColumnInIntervalKalkulator selektywność jest używany. Nie wiem dokładnie, kiedy to jest używane, ale wydaje się, że pojawia się znacznie częściej, gdy tabela bazowa jest stertą.

  2. Selektywność tranzytowa = 1. Innymi słowy, CASEoczekuje się , że jedno z wyrażeń zostanie ocenione jako fałszywe dla wszystkich wierszy. Nie ma znaczenia, czy pierwsze CASEwyrażenie ma wartość true dla wszystkich wierszy.

  3. Istnieje połączenie zewnętrzne z CStCollBaseTable. Innymi słowy, CASEwyrażenie wynikowe jest podzapytaniem na tabelę. Stała wartość nie będzie działać.

Być może w tych warunkach optymalizator kwerendy niezamierzenie stosuje selektywność tranzytową do oszacowania wiersza tabeli zewnętrznej zamiast do pracy wykonanej na wewnętrznej części zagnieżdżonej pętli. Zmniejszyłoby to oszacowanie wiersza do 1.

Udało mi się znaleźć dwa obejścia. Nie mogłem odtworzyć problemu, gdy użyłem APPLYzamiast podzapytania. Wyjście flagi śledzenia 2363 było bardzo różne APPLY. Oto jeden ze sposobów przepisania oryginalnej kwerendy w pytaniu:

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

dobre zapytanie 1

Starsza wersja CE wydaje się również unikać tego problemu.

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'));

dobre zapytanie 2

W tym wydaniu przesłano element połączeniowy (z niektórymi szczegółami, które Paul White podał w swojej odpowiedzi).

Joe Obbish
źródło