Par défaut, je suis toujours NOT EXISTS
.
Les plans d'exécution peuvent être les mêmes pour le moment, mais si l'une ou l'autre colonne est modifiée à l'avenir pour autoriser NULL
s le NOT IN
la version devra faire plus de travail (même si aucun NULL
s sont réellement présents dans les données) et la sémantique de NOT IN
si NULL
s sont présents sont peu susceptibles d'être ceux que vous voulez de toute façon.
Lorsque ni Products.ProductID
ou [Order Details].ProductID
autoriser NULL
s le NOT IN
sera traité de la même manière que la requête suivante.
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
Le plan exact peut varier, mais pour mes données d'exemple, j'obtiens ce qui suit.
Une idée fausse assez courante semble être que les sous-requêtes corrélées sont toujours "mauvaises" par rapport aux jointures. Ils peuvent certainement l'être lorsqu'ils forcent un plan de boucles imbriquées (sous requête évaluée ligne par ligne) mais ce plan inclut un opérateur logique anti semi join. Les anti-semi-jointures ne sont pas limitées aux boucles imbriquées, mais peuvent également utiliser des jointures de hachage ou de fusion (comme dans cet exemple).
/*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
Si [Order Details].ProductID
est NULL
-able la requête devient alors
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)
La raison en est que la sémantique correcte si [Order Details]
contient n'importe quel NULL
ProductId
s est de ne renvoyer aucun résultat. Consultez le spool supplémentaire anti-semi join et row count pour vérifier ce qui est ajouté au plan.
Si Products.ProductID
est également modifié pour devenir NULL
-able la requête devient alors
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)
La raison en est qu'un NULL
Products.ProductId
ne doit pas être renvoyé dans les résultats sauf si le NOT IN
la sous-requête devait ne renvoyer aucun résultat (c'est-à-dire le [Order Details]
le tableau est vide). Auquel cas ça devrait. Dans le plan de mes exemples de données, cela est implémenté en ajoutant une autre jointure anti-semi comme ci-dessous.
L'effet de ceci est montré dans le billet de blog déjà lié par Buckley. Dans l'exemple, le nombre de lectures logiques passe d'environ 400 à 500 000.
De plus, le fait qu'un seul NULL
peut réduire le nombre de lignes à zéro rend l'estimation de la cardinalité très difficile. Si SQL Server suppose que cela se produira mais qu'en fait il n'y avait pas de NULL
lignes dans les données, le reste du plan d'exécution peut être catastrophiquement pire, s'il ne s'agit que d'une partie d'une requête plus large, avec des boucles imbriquées inappropriées provoquant l'exécution répétée d'un sous-arbre coûteux, par exemple.
Ce n'est pas le seul plan d'exécution possible pour un NOT IN
sur un NULL
-able colonne cependant. Cet article en montre un autre pour une requête sur AdventureWorks2008
base de données.
Pour le NOT IN
sur un NOT NULL
ou la colonne NOT EXISTS
contre une colonne nullable ou non nullable, il donne le plan suivant.
Lorsque la colonne devient NULL
-able le NOT IN
le plan ressemble maintenant à
Il ajoute un opérateur de jointure interne supplémentaire au plan. Cet appareil est expliqué ici. Tout est là pour convertir la recherche d'index corrélé unique précédente sur Sales.SalesOrderDetail.ProductID = <correlated_product_id>
à deux recherches par rangée extérieure. L'autre est sur WHERE Sales.SalesOrderDetail.ProductID IS NULL
.
Comme il s'agit d'une jointure anti-semi si celle-ci renvoie des lignes, la deuxième recherche ne se produira pas. Cependant si Sales.SalesOrderDetail
ne contient aucun NULL
ProductId
s cela doublera le nombre d'opérations de recherche requises.