Ich verwende standardmäßig immer NOT EXISTS
.
Die Ausführungspläne können im Moment dieselben sein, aber wenn eine der Spalten in der Zukunft geändert wird, um NULL
zuzulassen s NOT IN
Version mehr Arbeit leisten muss (selbst wenn kein NULL
s tatsächlich in den Daten vorhanden sind) und die Semantik von NOT IN
wenn NULL
s sind anwesend sind wahrscheinlich sowieso nicht die, die Sie wollen.
Wenn weder Products.ProductID
oder [Order Details].ProductID
NULL
zulassen s NOT IN
wird genauso behandelt wie die folgende Abfrage.
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
Der genaue Plan kann variieren, aber für meine Beispieldaten erhalte ich Folgendes.
Ein ziemlich weit verbreitetes Missverständnis scheint zu sein, dass korrelierte Unterabfragen im Vergleich zu Joins immer "schlecht" sind. Sie können es sicherlich sein, wenn sie einen Plan mit verschachtelten Schleifen erzwingen (Unterabfrage wird Zeile für Zeile ausgewertet), aber dieser Plan enthält einen logischen Anti-Semi-Join-Operator. Anti-Semi-Joins sind nicht auf verschachtelte Schleifen beschränkt, sondern können auch Hash- oder Merge-Joins (wie in diesem Beispiel) verwenden.
/*Not valid syntax but better reflects the plan*/
SELECT p.ProductID,
p.ProductName
FROM Products p
LEFT ANTI SEMI JOIN [Order Details] od
ON p.ProductId = od.ProductId
Wenn [Order Details].ProductID
ist NULL
-fähig wird die Abfrage dann
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
AND NOT EXISTS (SELECT *
FROM [Order Details]
WHERE ProductId IS NULL)
Der Grund dafür ist die korrekte Semantik von [Order Details]
enthält beliebiges NULL
ProductId
s soll keine Ergebnisse zurückgeben. Sehen Sie sich den zusätzlichen Anti-Semi-Join- und Zeilenzähl-Spool an, um zu überprüfen, ob dies dem Plan hinzugefügt wurde.
Wenn Products.ProductID
wird ebenfalls in NULL
geändert -fähig wird die Abfrage dann
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
AND NOT EXISTS (SELECT *
FROM [Order Details]
WHERE ProductId IS NULL)
AND NOT EXISTS (SELECT *
FROM (SELECT TOP 1 *
FROM [Order Details]) S
WHERE p.ProductID IS NULL)
Der Grund dafür ist ein NULL
Products.ProductId
sollte außer nicht in den Ergebnissen zurückgegeben werden wenn der NOT IN
Unterabfrage überhaupt keine Ergebnisse zurückgeben (d. h. die [Order Details]
Tisch ist leer). In diesem Fall sollte es. Im Plan für meine Beispieldaten wird dies durch Hinzufügen eines weiteren Anti-Semi-Joins wie unten implementiert.
Wie sich das auswirkt, zeigt der von Buckley bereits verlinkte Blogbeitrag. Im dortigen Beispiel erhöht sich die Anzahl der logischen Lesezugriffe von etwa 400 auf 500.000.
Hinzu kommt die Tatsache, dass ein einzelnes NULL
die Zeilenanzahl auf null reduzieren kann, macht die Kardinalitätsschätzung sehr schwierig. Wenn SQL Server davon ausgeht, dass dies passieren wird, aber tatsächlich kein NULL
vorhanden war Zeilen in den Daten kann der Rest des Ausführungsplans katastrophal schlechter sein, wenn dies nur Teil einer größeren Abfrage ist, mit unangemessenen verschachtelten Schleifen, die beispielsweise die wiederholte Ausführung eines teuren Teilbaums verursachen.
Dies ist nicht der einzig mögliche Ausführungsplan für ein NOT IN
auf NULL
-fähigen Spalte jedoch. Dieser Artikel zeigt einen anderen für eine Abfrage gegen AdventureWorks2008
Datenbank.
Für NOT IN
auf einem NOT NULL
Spalte oder NOT EXISTS
gegen eine nullable oder non-nullable Spalte gibt es den folgenden Plan.
Wenn sich die Spalte zu NULL
ändert -fähig NOT IN
Plan sieht jetzt so aus
Es fügt dem Plan einen zusätzlichen inneren Join-Operator hinzu. Dieser Apparat wird hier erklärt. Es ist alles vorhanden, um die vorherige einzelne korrelierte Indexsuche auf Sales.SalesOrderDetail.ProductID = <correlated_product_id>
umzuwandeln auf zwei Suchvorgänge pro äußere Reihe. Die zusätzliche ist auf WHERE Sales.SalesOrderDetail.ProductID IS NULL
.
Da dies unter einem Anti-Semi-Join erfolgt, wenn dieser Zeilen zurückgibt, wird die zweite Suche nicht ausgeführt. Wenn jedoch Sales.SalesOrderDetail
kein NULL
enthält ProductID
s Dadurch wird die Anzahl der erforderlichen Suchvorgänge verdoppelt.