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 CASE
gibt, 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. CSelCalcColumnInInterval
erscheint 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:
Der CSelCalcColumnInInterval
Selektivitä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.
Pass-Through-Selektivität = 1. Mit anderen Worten, es CASE
wird erwartet , dass einer der Ausdrücke für alle Zeilen mit false bewertet wird. Es spielt keine Rolle, ob der erste CASE
Ausdruck für alle Zeilen als wahr ausgewertet wird.
Es gibt eine äußere Verbindung zu CStCollBaseTable
. Mit anderen Worten ist der CASE
Ergebnisausdruck 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 APPLY
anstelle 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);
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'));
Für dieses Problem wurde ein Verbindungselement gesendet (mit einigen Details, die Paul White in seiner Antwort angegeben hat).