Eu sempre uso como padrão
NOT EXISTS
. Os planos de execução podem ser os mesmos no momento, mas se qualquer coluna for alterada no futuro para permitir
NULL
é o NOT IN
terá de trabalhar mais (mesmo se não houver NULL
s estão realmente presentes nos dados) e a semântica de NOT IN
se NULL
s são presentes são improváveis de serem os que você quer de qualquer maneira. Quando nem
Products.ProductID
ou [Order Details].ProductID
permitir NULL
é o NOT IN
será tratado de forma idêntica à consulta a seguir. SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
O plano exato pode variar, mas para meus dados de exemplo, recebo o seguinte.
Um equívoco razoavelmente comum parece ser que as subconsultas correlacionadas são sempre "ruins" em comparação com as junções. Eles certamente podem ser quando forçam um plano de loops aninhados (subconsulta avaliada linha por linha), mas esse plano inclui um operador lógico anti-semi-junção. Anti semijunções não são restritas a loops aninhados, mas também podem usar junções de hash ou mesclagem (como neste exemplo).
/*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
Se
[Order Details].ProductID
é NULL
-able a consulta então se torna 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)
A razão para isso é que a semântica correta se
[Order Details]
contém qualquer NULL
ProductId
s é não retornar nenhum resultado. Consulte o spool de contagem de linhas e anti semijunção extra para verificar se isso é adicionado ao plano. Se
Products.ProductID
também é alterado para se tornar NULL
-able a consulta então se torna 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)
A razão para isso é porque um
NULL
Products.ProductId
não deve ser retornado nos resultados exceto se o NOT IN
subconsulta não retornasse nenhum resultado (ou seja, o [Order Details]
tabela está vazia). Nesse caso deveria. No plano para meus dados de exemplo, isso é implementado adicionando outra anti semijunção, conforme abaixo. O efeito disso é mostrado na postagem do blog já vinculada por Buckley. No exemplo, o número de leituras lógicas aumenta de cerca de 400 para 500.000.
Além disso, o fato de um único
NULL
pode reduzir a contagem de linhas para zero torna a estimativa de cardinalidade muito difícil. Se o SQL Server assumir que isso acontecerá, mas na verdade não houve NULL
linhas nos dados, o resto do plano de execução pode ser catastroficamente pior, se isso for apenas parte de uma consulta maior, com loops aninhados inadequados causando a execução repetida de uma subárvore cara, por exemplo. Este não é o único plano de execução possível para um
NOT IN
em um NULL
-able coluna no entanto. Este artigo mostra outro para uma consulta no AdventureWorks2008
base de dados. Para o
NOT IN
em um NOT NULL
coluna ou NOT EXISTS
em relação a uma coluna anulável ou não anulável, ele fornece o plano a seguir. Quando a coluna muda para
NULL
-ativa o NOT IN
plano agora parece Ele adiciona um operador de junção interna extra ao plano. Este aparelho é explicado aqui. Está tudo lá para converter a busca de índice correlacionado único anterior em
Sales.SalesOrderDetail.ProductID = <correlated_product_id>
a duas buscas por linha externa. O adicional está em WHERE Sales.SalesOrderDetail.ProductID IS NULL
. Como isso está sob uma anti semi-junção, se essa retornar qualquer linha, a segunda busca não ocorrerá. No entanto, se
Sales.SalesOrderDetail
não contém nenhum NULL
ProductID
s dobrará o número de operações de busca necessárias.