NOT IN与NOT EXISTS的区别

628

哪个查询更快?

NOT EXISTS:

SELECT ProductID, ProductName 
FROM Northwind..Products p
WHERE NOT EXISTS (
    SELECT 1 
    FROM Northwind..[Order Details] od 
    WHERE p.ProductId = od.ProductId)

或者不在:

SELECT ProductID, ProductName 
FROM Northwind..Products p
WHERE p.ProductID NOT IN (
    SELECT ProductID 
    FROM Northwind..[Order Details])

查询执行计划显示它们做的是相同的事情。如果是这样,哪种形式是推荐的?基于NorthWind数据库。

[编辑]

刚刚发现这篇有用的文章:http://weblogs.sqlteam.com/mladenp/archive/2007/05/18/60210.aspx

我想我会坚持使用NOT EXISTS。


更快?结果不同(至少在某些情况下)。 - jarlh
12个回答

783
我总是默认使用NOT EXISTS
执行计划目前可能相同,但如果将来更改任一列以允许NULLNOT IN版本将需要做更多的工作(即使实际上数据中没有NULL),而且如果存在NULLNOT IN的语义也不太可能是您想要的。
Products.ProductID[Order Details].ProductID都不允许NULL时,NOT IN将被视为以下查询的副本。
SELECT ProductID,
       ProductName
FROM   Products p
WHERE  NOT EXISTS (SELECT *
                   FROM   [Order Details] od
                   WHERE  p.ProductId = od.ProductId) 

准确的计划可能有所不同,但对于我的示例数据,我得到了以下结果。

Neither NULL

一个相当普遍的误解似乎是相关子查询与连接相比总是“不好的”。当它们强制使用嵌套循环计划(逐行评估子查询)时,它们确实可能会有问题,但是该计划包括一个反半连接逻辑运算符。反半连接不仅限于嵌套循环,还可以使用哈希或合并(如此示例中)连接。
/*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 

如果[订单详情].ProductID可以为NULL,则查询将变为:
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) 

这是因为,如果[Order Details]包含任何NULLProductId,则正确的语义是返回零结果。请查看计划中添加的额外反半连接和行计数池以验证此内容的涵义。

One NULL

如果将 Products.ProductID 也改为可空,则查询变为:
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) 

那么这样做的原因是,除非NOT IN子查询没有返回任何结果(即[Order Details]表为空),否则不应该在结果中返回NULL Products.ProductId。如果是这种情况,则应该返回NULL。在我的示例数据计划中,通过添加另一个反半连接来实现此目的。

Both NULL

这样做的效果在Buckley已经链接的博客文章中展示。在那个例子中,逻辑读取次数从大约400增加到500,000。

此外,单个NULL可以将行数减少到零,使基数估计非常困难。如果SQL Server假定会发生这种情况,但实际上数据中没有NULL行,则执行计划的其余部分可能会灾难性地恶化,如果这只是更大查询的一部分,例如造成不适当的嵌套循环导致昂贵的子树重复执行

然而,这不是NULL列上的NOT IN的唯一可能执行计划。本文显示了另一个针对AdventureWorks2008数据库的查询的执行计划。

针对非空列的NOT IN或针对可空或非空列的NOT EXISTS,将提供以下计划。

Not EXists

当列变为可以为NULL时,NOT IN计划现在的样子是这样的:

Not In - Null

它向计划添加了一个额外的内连接运算符。这个工具在此处解释。它旨在将以前对Sales.SalesOrderDetail.ProductID = <correlated_product_id>的单个相关索引搜索转换为每个外部行两次搜索。另一个搜索是在WHERE Sales.SalesOrderDetail.ProductID IS NULL上执行的。
由于这是在反半连接下进行的,如果该操作返回任何行,则不会发生第二个搜索。但是,如果Sales.SalesOrderDetail不包含任何NULL ProductID,则需要的搜索操作数量将增加一倍。

100
请注意,当涉及到null时,NOT IN与NOT EXISTS不等同。这篇帖子解释得非常好。

http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/

When the subquery returns even one null, NOT IN will not match any rows.

The reason for this can be found by looking at the details of what the NOT IN operation actually means.

Let’s say, for illustration purposes that there are 4 rows in the table called t, there’s a column called ID with values 1..4

WHERE SomeValue NOT IN (SELECT AVal FROM t)

is equivalent to

WHERE SomeValue != (SELECT AVal FROM t WHERE ID=1)
AND SomeValue != (SELECT AVal FROM t WHERE ID=2)
AND SomeValue != (SELECT AVal FROM t WHERE ID=3)
AND SomeValue != (SELECT AVal FROM t WHERE ID=4)

Let’s further say that AVal is NULL where ID = 4. Hence that != comparison returns UNKNOWN. The logical truth table for AND states that UNKNOWN and TRUE is UNKNOWN, UNKNOWN and FALSE is FALSE. There is no value that can be AND’d with UNKNOWN to produce the result TRUE

Hence, if any row of that subquery returns NULL, the entire NOT IN operator will evaluate to either FALSE or NULL and no records will be returned


25

如果执行计划器说它们是一样的,那它们就是一样的。使用让你的意图更加明显的那个 - 在这种情况下,就是第二个。


4
执行计划的时间可能相同,但执行结果可能不同,因此有所区别。如果数据集中包含 NULL,则 NOT IN 将产生意外的结果(请参见 buckley 的答案)。 最好将 NOT EXISTS 作为默认选项。 - nanonerd

19

实际上,我认为这可能是最快的方法:

SELECT ProductID, ProductName 
    FROM Northwind..Products p  
          outer join Northwind..[Order Details] od on p.ProductId = od.ProductId)
WHERE od.ProductId is null

2
当优化器在执行任务时,可能不是最快的,但当它没有执行任务时,肯定会更快。 - Cade Roux
2
他可能为了这篇文章简化了他的查询。 - Kip
1
同意左外连接通常比子查询更快。 - HLGEM
7
不同意。根据我的经验,左外连接的最佳情况是它们与内连接相同,而SQL Server会将左外连接转换为反半连接。在最坏的情况下,SQL Server会将所有左外连接并在过滤掉 NULL 值之后,这可能效率更低。本文底部提供了一个示例 - Martin Smith
1
刚刚登录以点赞您的回答,先生。我也在寻找同样的问题,我的查询从使用子查询需要4分钟变成了使用全外连接和WHERE中的IS NULL只需要1秒钟。 - Luiz Carlos

14

我有一个表格,其中有大约120,000条记录,需要选择那些与四个其他表格(每个表格的行数约为1500、4000、40000和200)中的某个Varchar列不匹配的记录。所有涉及的表格都在相关的Varchar列上拥有唯一索引。

NOT IN花费了大约10分钟,而NOT EXISTS只花费了4秒钟。

我有一个递归查询,可能有一些未调整的部分导致了10分钟的时间,但是另一个选项只用了4秒钟,这至少对我来说说明NOT EXISTS要好得多,或者至少INEXISTS并不完全相同,在进行编码之前值得检查。


9
我正在使用
SELECT * from TABLE1 WHERE Col1 NOT IN (SELECT Col1 FROM TABLE2)

我发现它给出了错误的结果(我的意思是没有结果)。因为TABLE2.Col1中有一个NULL。

将查询更改为

SELECT * from TABLE1 T1 WHERE NOT EXISTS (SELECT Col1 FROM TABLE2 T2 WHERE T1.Col1 = T2.Col2)

给我正确的结果。

从那时起,我已经开始在各个地方使用NOT EXISTS。


8
在您的具体示例中,它们是相同的,因为优化器已经确定您在两个示例中尝试执行的操作是相同的。但是,在非平凡的示例中,优化器可能无法做到这一点,在这种情况下,有时有理由更喜欢其中一个。
如果在外部选择中测试多行,则应首选NOT INNOT IN语句内部的子查询可以在执行开始时进行评估,并且可以针对外部选择中的每个值检查临时表,而不需要像使用NOT EXISTS语句那样每次重新运行子查询。
如果子查询必须与外部选择相关,则可能更喜欢NOT EXISTS,因为优化器可能会发现简化,从而防止创建任何临时表来执行相同的功能。

7

数据库表模型

假设我们的数据库中存在以下两个表,它们形成了一对多的表关系。

SQL EXISTS tables

student 表是父表,student_grade 是子表,因为它有一个 student_id 外键列引用 student 表的 id 主键列。

student 表包含以下两条记录:

id first_name last_name admission_score
1 Alice Smith 8.95
2 Bob Johnson 8.75

student_grade 表存储学生获得的成绩:

id class_name grade student_id
1 Math 10 1
2 Math 9.5 1
3 Math 9.75 1
4 Science 9.5 1
5 Science 9 1
6 Science 9.25 1
7 Math 8.5 2
8 Math 9.5 2
9 Math 9 2
10 Science 10 2
11 Science 9.4 2

SQL EXISTS

假设我们想要获取所有在数学课程中获得10分的学生。

如果我们只关心学生的标识符,那么我们可以运行以下查询语句:

SELECT
    student_grade.student_id
FROM
    student_grade
WHERE
    student_grade.grade = 10 AND
    student_grade.class_name = 'Math'
ORDER BY
    student_grade.student_id

然而,该应用程序希望显示学生的全名,而不仅仅是标识符,因此我们还需要从学生表中获取信息。

为了过滤在数学成绩上获得10分的学生记录,我们可以使用EXISTS SQL运算符,如下所示:

SELECT
    id, first_name, last_name
FROM
    student
WHERE EXISTS (
    SELECT 1
    FROM
        student_grade
    WHERE
        student_grade.student_id = student.id AND
        student_grade.grade = 10 AND
        student_grade.class_name = 'Math'
)
ORDER BY id

运行以上查询时,我们可以看到仅选择了Alice行:
id first_name last_name 1 Alice Smith
外部查询选择了我们想要返回给客户端的student行列。然而,WHERE子句使用了EXISTS运算符和相关联的内部子查询。
如果子查询返回至少一条记录,则EXISTS运算符返回true,否则返回false。数据库引擎并不必须完全运行子查询。如果匹配到一条记录,则EXISTS运算符返回true,并选择相关联的其他查询行。
内部子查询是相关联的,因为student_grade表的student_id列与外部student表的id列进行了匹配。
SQL NOT EXISTS
假设我们希望选择所有成绩不低于9分的学生。为此,我们可以使用NOT EXISTS运算符,该运算符否定了EXISTS运算符的逻辑。
因此,如果底层子查询没有返回任何记录,则NOT EXISTS运算符返回true。但是,如果内部子查询匹配到一条记录,则NOT EXISTS运算符将返回false,并且可以停止子查询执行。
为了匹配所有没有与小于9分关联的student_grade的学生记录,我们可以运行以下SQL查询:
SELECT
    id, first_name, last_name
FROM
    student
WHERE NOT EXISTS (
    SELECT 1
    FROM
        student_grade
    WHERE
        student_grade.student_id = student.id AND
        student_grade.grade < 9
)
ORDER BY id

运行上述查询时,我们可以看到只有 Alice 的记录被匹配:
id first_name last_name
1 Alice Smith
因此,使用 SQL EXISTS 和 NOT EXISTS 操作符的优点在于,只要找到匹配的记录,内部子查询的执行就会停止。

4

它们非常相似,但并不完全相同。

就效率而言,当需要选择大量行时,我发现 left join is null 语句更有效。


2

如果优化器说它们是相同的,那么请考虑人为因素。我更喜欢看到 NOT EXISTS :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接