Warum reduziert eine Unterabfrage die Zeilenschätzung auf 1?

26

Betrachten Sie die folgende erfundene, aber einfache Abfrage:

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;

Ich würde erwarten, dass die endgültige Zeilenschätzung für diese Abfrage der Anzahl der Zeilen in der X_HEAPTabelle entspricht. Was auch immer ich in der Unterabfrage tue, sollte für die Zeilenschätzung keine Rolle spielen, da keine Zeilen herausgefiltert werden können. In SQL Server 2016 wird die Zeilenschätzung jedoch aufgrund der Unterabfrage auf 1 reduziert:

schlechte Abfrage

Warum passiert das? Was kann ich tun?

Es ist sehr einfach, dieses Problem mit der richtigen Syntax zu reproduzieren. Hier ist eine Reihe von Tabellendefinitionen, die dies ermöglichen:

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 Geige Link .

Joe Obbish
quelle

Antworten:

22

Diese Cardinality Estimation (CE) gibt Oberflächen aus, wenn:

  1. Der Join ist ein Outer- Join mit einem Pass-Through- Prädikat
  2. Die Selektivität des Pass-Through-Prädikats wird auf genau 1 geschätzt .

Hinweis: Der spezielle Rechner, der zur Bestimmung der Selektivität verwendet wird, ist nicht wichtig.


Einzelheiten

Das CE berechnet die Selektivität des äußeren Joins als die Summe von:

  • Die innere Join- Selektivität mit demselben Prädikat
  • Die Anti-Join- Selektivität mit demselben Prädikat

Der einzige Unterschied zwischen einem äußeren und einem inneren Join besteht darin, dass ein äußerer Join auch Zeilen zurückgibt, die im Join-Prädikat nicht übereinstimmen. Das Anti-Join bietet genau diesen Unterschied. Die Kardinalitätsschätzung für die innere und die Anti-Verknüpfung ist einfacher als für die direkte äußere Verknüpfung.

Der Prozess zur Schätzung der Join-Selektivität ist sehr einfach:

  • Zunächst wird die Selektivität des Pass-Through-Prädikats bewertet. SPT
    • Dies geschieht mit dem für die jeweiligen Umstände geeigneten Taschenrechner.
    • Das Prädikat ist das Ganze, einschließlich aller negierenden IsFalseOrNullKomponenten.
  • Innere Join-Selektivität: = 1 - SPT
  • Anti-Join-Selektivität: = SPT

Die Anti-Verknüpfung stellt Zeilen dar, die die Verknüpfung "durchlaufen". Die innere Verknüpfung stellt Zeilen dar, die nicht durchlaufen werden. Beachten Sie, dass "Durchlaufen" Zeilen bedeutet, die durch die Verbindung fließen, ohne dass die Innenseite überhaupt ausgeführt wird. Zur Hervorhebung: Alle Zeilen werden vom Join zurückgegeben. Es wird unterschieden zwischen Zeilen, die vor dem Auftauchen an der Innenseite des Joins ausgeführt werden, und solchen, die dies nicht tun.

Das Hinzufügen von zu sollte natürlich immer eine Gesamtselektivität von 1 ergeben, was bedeutet, dass alle Zeilen vom Join wie erwartet zurückgegeben werden.1 - SPTSPT

In der Tat funktioniert die obige Berechnung genau wie für alle Werte außer 1 beschrieben .SPT

Wenn = 1, werden die Selektivitäten für innere Verknüpfungen und Anti-Verknüpfungen auf Null geschätzt, was zu einer Kardinalitätsschätzung (für die Verknüpfung als Ganzes) einer Zeile führt. Soweit ich das beurteilen kann, ist dies unbeabsichtigt und sollte als Fehler gemeldet werden.SPT


Ein verwandtes Problem

Es ist wahrscheinlicher, dass dieser Fehler aufgrund einer separaten CE-Einschränkung auftritt, als man denkt. Dies tritt auf, wenn der CASEAusdruck EXISTS(wie üblich) eine Klausel verwendet . Die folgende geänderte Abfrage aus der Frage trifft beispielsweise nicht auf die unerwartete Kardinalitätsschätzung:

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

Das Einführen einer Trivialität EXISTSführt dazu, dass das Problem auftritt:

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

Mit wird EXISTSein Semi-Join (hervorgehoben) zum Ausführungsplan hinzugefügt:

Semi-Join-Plan

Die Schätzung für den Semi-Join ist in Ordnung. Das Problem ist, dass der CE die zugehörige Sondensäule als einfache Projektion mit einer festen Selektivität von 1 behandelt:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

Dies erfüllt automatisch eine der Bedingungen, die erforderlich sind, damit sich dieses CE-Problem manifestiert, unabhängig vom Inhalt der EXISTSKlausel.


Wichtige Hintergrundinformationen finden Sie unter Unterabfragen in CASEAusdrücken von Craig Freedman.

Paul White sagt GoFundMonica
quelle
22

Dies scheint definitiv ein ungewolltes Verhalten zu sein. Es ist richtig, dass Kardinalitätsschätzungen nicht bei jedem Schritt eines Plans konsistent sein müssen. Dies ist jedoch ein relativ einfacher Abfrageplan, und die endgültige Kardinalitätsschätzung stimmt nicht mit der Ausführung der Abfrage überein. Eine solche Schätzung der geringen Kardinalität könnte zu einer schlechten Auswahl von Verknüpfungstypen und Zugriffsmethoden für andere nachgeschaltete Tabellen in einem komplizierteren Plan führen.

Durch Ausprobieren können wir einige ähnliche Abfragen erstellen, für die das Problem nicht auftritt:

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;

Wir können auch weitere Fragen stellen, bei denen das Problem auftritt:

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;

Es scheint ein Muster zu geben: Wenn es einen Ausdruck in dem CASEgibt, von dem nicht erwartet wird, dass er ausgeführt wird, und der Ergebnisausdruck eine Unterabfrage für eine Tabelle ist, fällt die Zeilenschätzung nach diesem Ausdruck auf 1.

Wenn ich die Abfrage für eine Tabelle mit einem Clustered-Index schreibe, ändern sich die Regeln etwas. Wir können die gleichen Daten verwenden:

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;

Diese Abfrage hat eine endgültige Schätzung von 1000 Zeilen:

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;

Diese Abfrage hat jedoch eine letzte Schätzung von 1 Zeile:

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;

Um dies weiter zu untersuchen, können wir das undokumentierte Ablaufverfolgungsflag 2363 verwenden , um Informationen darüber zu erhalten, wie der Abfrageoptimierer Selektivitätsberechnungen durchgeführt hat. Ich fand es hilfreich, dieses Ablaufverfolgungsflag mit dem undokumentierten Ablaufverfolgungsflag 8606 zu koppeln . TF 2363 scheint Selektivitätsberechnungen sowohl für den vereinfachten Baum als auch für den Baum nach der Projektnormalisierung zu liefern. Wenn beide Ablaufverfolgungsflags aktiviert sind, wird deutlich, welche Berechnungen für welchen Baum gelten.

Versuchen wir es mit der ursprünglichen Abfrage in der Frage:

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

Hier ist ein Teil des Teils der Ausgabe, den ich zusammen mit einigen Kommentaren für relevant halte:

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)

Versuchen wir es jetzt mit einer ähnlichen Abfrage, bei der das Problem nicht auftritt. Ich werde dieses verwenden:

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

Debug-Ausgabe ganz am Ende:

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

Versuchen wir es mit einer anderen Abfrage, für die die falsche Zeilenschätzung vorliegt:

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

Ganz am Ende fällt die Kardinalitätsschätzung nach Pass-Through-Selektivität = 1 wieder auf 1 Zeile. Die Kardinalitätsschätzung bleibt nach einer Selektivität von 0,501 und 0,499 erhalten.

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)

Wechseln wir noch einmal zu einer ähnlichen Abfrage, bei der das Problem nicht auftritt. Ich werde dieses verwenden:

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

In der Debug-Ausgabe gibt es niemals einen Schritt mit einer Pass-Through-Selektivität von 1. Die Kardinalitätsschätzung bleibt bei 1000 Zeilen.

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

Was ist mit der Abfrage, wenn es sich um eine Tabelle mit einem Clustered-Index handelt? Betrachten Sie die folgende Abfrage mit dem Problem der Zeilenschätzung:

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

Das Ende der Debug-Ausgabe ähnelt dem, was wir bereits gesehen haben:

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)

Die Abfrage für das CI ohne das Problem hat jedoch eine andere Ausgabe. Mit dieser Abfrage:

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

Führt dazu, dass verschiedene Rechner verwendet werden. CSelCalcColumnInIntervalerscheint nicht mehr:

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)

Zusammenfassend scheinen wir nach der Unterabfrage unter den folgenden Bedingungen eine falsche Zeilenschätzung zu erhalten:

  1. Der CSelCalcColumnInIntervalSelektivitätsrechner wird verwendet. Ich weiß nicht genau, wann dies verwendet wird, aber es scheint sich viel häufiger zu zeigen, wenn der Basistisch ein Haufen ist.

  2. Pass-Through-Selektivität = 1. Mit anderen Worten, es CASEwird erwartet , dass einer der Ausdrücke für alle Zeilen mit false bewertet wird. Es spielt keine Rolle, ob der erste CASEAusdruck für alle Zeilen als wahr ausgewertet wird.

  3. Es gibt eine äußere Verbindung zu CStCollBaseTable. Mit anderen Worten ist der CASEErgebnisausdruck eine Unterabfrage für eine Tabelle. Ein konstanter Wert funktioniert nicht.

Unter diesen Umständen wendet das Abfrageoptimierungsprogramm die Durchgriffsselektivität möglicherweise unbeabsichtigt auf die Zeilenschätzung der äußeren Tabelle an, anstatt auf die Arbeit, die im inneren Teil der verschachtelten Schleife ausgeführt wird. Das würde die Zeilenschätzung auf 1 reduzieren.

Ich konnte zwei Problemumgehungen finden. Ich konnte das Problem nicht reproduzieren, wenn ich APPLYanstelle einer Unterabfrage verwende. Die Ausgabe des Trace-Flags 2363 war bei sehr unterschiedlich APPLY. Hier ist eine Möglichkeit, die ursprüngliche Abfrage in der Frage umzuschreiben:

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

gute abfrage 1

Das ältere CE scheint das Problem ebenfalls zu umgehen.

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

gute Abfrage 2

Für dieses Problem wurde ein Verbindungselement gesendet (mit einigen Details, die Paul White in seiner Antwort angegeben hat).

Joe Obbish
quelle