Ich habe oft gelesen, dass man die Existenz einer Zeile immer mit EXISTS anstatt mit COUNT überprüfen muss.
Es ist sehr selten, dass etwas immer wahr ist, besonders wenn es um Datenbanken geht. Es gibt eine beliebige Anzahl von Möglichkeiten, dieselbe Semantik in SQL auszudrücken. Wenn es eine nützliche Faustregel gibt, kann es sein, dass Abfragen mit der natürlichsten verfügbaren Syntax (und ja, das ist subjektiv) geschrieben werden und nur Überschreibungen in Betracht gezogen werden, wenn der Abfrageplan oder die Leistung, die Sie erhalten, nicht akzeptabel sind.
Ich gehe davon aus, dass Existenzfragen am natürlichsten ausgedrückt werden, wenn man sie verwendet EXISTS
. Es ist auch meine Erfahrung, die EXISTS
dazu neigt, besser zu optimieren als die OUTER JOIN
Ausschussalternative NULL
. Verwenden COUNT(*)
und Filtern nach =0
ist eine weitere Alternative, die im SQL Server-Abfrageoptimierungsprogramm Unterstützung bietet. Ich persönlich habe jedoch festgestellt, dass dies bei komplexeren Abfragen unzuverlässig ist. In jedem Fall EXISTS
scheint mir das viel natürlicher zu sein als jede dieser Alternativen.
Ich habe mich gefragt, ob es einen nicht angekündigten Fehler bei EXISTS gibt, der den Messungen, die ich durchgeführt habe, vollkommen Sinn macht
Ihr spezielles Beispiel ist interessant, da es die Art und Weise hervorhebt, wie das Optimierungsprogramm mit Unterabfragen in CASE
Ausdrücken (und EXISTS
insbesondere Tests) umgeht .
Unterabfragen in CASE-Ausdrücken
Betrachten Sie die folgende (vollkommen legale) Abfrage:
DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);
SELECT
CASE
WHEN (SELECT W.b FROM @When AS W) = 1
THEN (SELECT T.c FROM @Then AS T)
ELSE (SELECT E.d FROM @Else AS E)
END
FROM @Base AS B;
Die Semantik vonCASE
ist, dass WHEN/ELSE
Klauseln im Allgemeinen in Textreihenfolge ausgewertet werden. In der obigen Abfrage wäre es falsch ELSE
, wenn SQL Server einen Fehler zurückgeben würde, wenn die Unterabfrage mehr als eine Zeile zurückgeben würde, wenn die WHEN
Klausel erfüllt wäre. Um diese Semantik zu berücksichtigen, erstellt das Optimierungsprogramm einen Plan, der Pass-Through-Prädikate verwendet:
Die Innenseite der Joins mit verschachtelten Schleifen wird nur ausgewertet, wenn das Pass-Through-Prädikat false zurückgibt. Der Gesamteffekt besteht darin, dass CASE
Ausdrücke der Reihe nach getestet werden und Unterabfragen nur ausgewertet werden, wenn kein vorheriger Ausdruck erfüllt wurde.
CASE-Ausdrücke mit einer EXISTS-Unterabfrage
Wenn eine CASE
Unterabfrage verwendet wird EXISTS
, wird der logische Existenztest als Semi-Join implementiert. Zeilen, die normalerweise vom Semi-Join zurückgewiesen werden, müssen jedoch beibehalten werden, falls sie in einer späteren Klausel benötigt werden. Zeilen, die diese spezielle Art von Semi-Join durchlaufen, erhalten ein Flag, das angibt, ob der Semi-Join eine Übereinstimmung gefunden hat oder nicht. Dieses Flag wird als Sondenspalte bezeichnet .
Die Details der Implementierung bestehen darin, dass die logische Unterabfrage durch einen korrelierten Join ('apply') mit einer Testspalte ersetzt wird. Die Arbeit wird durch eine Vereinfachungsregel im Abfrageoptimierer mit dem Namen RemoveSubqInPrj
(Unterabfrage in Projektion entfernen) ausgeführt. Wir können die Details mit dem Trace-Flag 8606 sehen:
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);
Ein Teil des Eingabebaums, der den EXISTS
Test zeigt, ist unten dargestellt:
ScaOp_Exists
LogOp_Project
LogOp_Select
LogOp_Get TBL: #T2
ScaOp_Comp x_cmpEq
ScaOp_Identifier [T2].ID
ScaOp_Identifier [T1].ID
Dies verwandelt sich RemoveSubqInPrj
in eine Struktur, die geleitet wird von:
LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)
Dies ist die linke Halbverbindung, die mit dem zuvor beschriebenen Prüfpunkt angewendet wird. Diese erste Umwandlung ist die einzige, die derzeit in SQL Server-Abfrageoptimierungsprogrammen verfügbar ist, und die Kompilierung schlägt einfach fehl, wenn diese Umwandlung deaktiviert ist.
Eine der möglichen Ausführungsplanformen für diese Abfrage ist eine direkte Implementierung dieser logischen Struktur:
Der endgültige Berechnungsskalar wertet das Ergebnis des CASE
Ausdrucks anhand des Werts der Prüfspalte aus:
Die Grundform des Planbaums bleibt erhalten, wenn die Optimierung andere physische Verknüpfungstypen für die Halbverknüpfung berücksichtigt. Nur der Merge-Join unterstützt eine Prüfspalte, sodass ein Hash-Semi-Join, obwohl logisch möglich, nicht berücksichtigt wird:
Beachten Sie, dass die Zusammenführung einen Ausdruck mit Expr1008
der Bezeichnung ausgibt (der Name ist derselbe wie zuvor, ein Zufall), obwohl für keinen Operator im Plan eine Definition dafür angezeigt wird. Dies ist wieder nur die Sondensäule. Nach wie vor verwendet der endgültige Berechnungsskalar diesen Prüfpunktwert zur Auswertung der CASE
.
Das Problem ist, dass das Optimierungsprogramm Alternativen nicht vollständig untersucht, die sich erst durch das Zusammenführen (oder Hash) von Semi-Joins lohnen. Im Plan mit verschachtelten Schleifen besteht kein Vorteil darin, bei T2
jeder Iteration zu überprüfen, ob die Zeilen im Bereich übereinstimmen. Bei einem Zusammenführungs- oder Hash-Plan kann dies eine nützliche Optimierung sein.
Wenn wir der Abfrage ein übereinstimmendes BETWEEN
Prädikat hinzufügen T2
, wird diese Prüfung lediglich für jede Zeile als Residuum des Merge-Semi-Joins ausgeführt (im Ausführungsplan schwer zu erkennen, aber vorhanden):
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
AND T2.ID BETWEEN 5000 AND 7000 -- New
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Wir würden hoffen, dass das BETWEEN
Prädikat stattdessen zu T2
einer Suche zurückgedrängt wird. Normalerweise würde das Optimierungsprogramm dies in Betracht ziehen (auch ohne das zusätzliche Prädikat in der Abfrage). Implizierte Prädikate ( BETWEEN
on T1
und das Join-Prädikat zwischen T1
und T2
zusammen implizieren das BETWEEN
on T2
) werden erkannt , ohne dass sie im ursprünglichen Abfragetext vorhanden sind. Leider bedeutet das Muster "Probe anwenden", dass dies nicht untersucht wird.
Es gibt Möglichkeiten, die Abfrage zu schreiben, um Suchvorgänge für beide Eingaben in einen Merge-Semi-Join zu erzeugen. Eine Möglichkeit besteht darin, die Abfrage auf eine ziemlich unnatürliche Weise zu schreiben (und dabei den Grund zu verneinen, den ich im Allgemeinen bevorzuge EXISTS
):
WITH T2 AS
(
SELECT TOP (9223372036854775807) *
FROM #T2 AS T2
WHERE ID BETWEEN 5000 AND 7000
)
SELECT
T1.ID,
DoesExist =
CASE
WHEN EXISTS
(
SELECT * FROM T2
WHERE T2.ID = T1.ID
) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Es würde mich nicht freuen, diese Abfrage in einer Produktionsumgebung zu schreiben. Sie soll nur zeigen, dass die gewünschte Planform möglich ist. Wenn die tatsächliche Abfrage, die Sie schreiben müssen, CASE
auf diese Weise verwendet wird und die Leistung darunter leidet, dass auf der Testseite eines Merge-Semi-Joins keine Suche erfolgt, können Sie die Abfrage mit einer anderen Syntax schreiben, die die richtigen Ergebnisse liefert, und a effizienter Ausführungsplan.