暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SQL为什么不建议使用not in子查询

SQLServer爱好者 2021-04-29
3811

在SQL Server中,子查询可以分为相关子查询和无关子查询,对于无关子查询来说,not in子句比较常见,但not in潜在会带来下面两种问题:

结果不准确

查询性能低下

下面我们来看一下为什么尽量不使用not in子句。

 

问题1:结果不准确

在SQL Server中,null值并不是一个值,而是表示特定含义,其所表示的含义是“Unknow”,可以理解为未定义或者未知,因此任何与null值进行比对的二元操作符结果一定为null,包括null值本身。

 

例1:

IF (NULL=NULL) PRINT 1

IF (NULL=1)PRINT 1

结果:无返回值,null值与任何值进行对比结果都为null。

注意:SQL Server提供了“IS”操作符与null值做对比,用于衡量某个值是否为null。

 

那么not in 的问题在哪呢?

例2:

SELECT '有值' WHERE 3 NOT IN(1,2,NULL)

结果:无返回值。难道不是返回有值吗?!

分析:条件“3”不属于not in后面列表的任意一个,该查询却不返回任何值。具体原因就是not in子句对于null值的处理,在SQLServer中,not in子句其实等价于如下子句(同样无返回值):

SELECT '有值' WHERE 3<>1 AND 3<>2 AND 3<>NULL

 

这里,not in转换为条件对于每个值进行不等<>比对,并用逻辑与(AND)连接起来,而前面提到过null值与任意其他值做比较时,结果永远为null,即3<>null为false,从而导致not in子句不返回任何行。

 

因此,如果not in子句的列表值中即使存在一个null值,就会导致结果不会返回任何数据。

 

解决办法:不使用not in,而用not exists代替。

exists的操作符不会返回null,只会根据子查询中的每一行决定返回true或者false,当遇到null值时,只会返回false,而不会由某个null值导致整个子查询表达式为null。对于上述查询,可以改为如下not exists子查询:

SELECT col1

FROM ( SELECT 3 AS col1 ) AS b

WHERE NOT EXISTS(

  SELECT *

  FROM ( SELECT 1 AS col2 UNION

         SELECT 2 UNION

         SELECT NULL ) AS a

  WHERE a.col2 = b.col1 )

结果:返回3

 

问题2:not in导致查询性能低下

前面我们可以看出,not in的主要问题是由于对null值的处理问题所导致,那么对null值的处理究竟为什么会导致性能问题?让我们来看如下not in子查询:

 

例3:

SELECT * FROM SalesOrderHeader

WHERE SalesOrderID NOT IN( SELECT ProductID FROM SalesOrderDetail )

这里,为了演示目的仅将SalesOrderDetail表的ProductID列的定义由not null改为null。查看执行计划可以看到一个Row Count Spool操作,该操作符用于确认ProductID列中是否有null值(过程是对比总行数和非null行数,不相等则为有null值,虽然我们知道该列中没有null值,但由于列定义是允许null的,因此SQL Server必须进行额外的确认),而该操作符占用了接近一半的查询成本。此时如果改用如下not exists查询:

SELECT * FROM SalesOrderHeader a

WHERE NOT EXISTS( SELECT * FROM SalesOrderDetail b WHERE a.SalesOrderID=b.ProductID)

结果:查看执行计划发现,not in的执行成本几乎是not exists的N倍,仅仅是由于SQLServer需要确认允许null列中是否存在null。


根据例2中not in的等价形式,可以得出与例3中not in等价的not exist形式:

SELECT * FROM SalesOrderHeader a

WHERE NOT EXISTS( SELECT * FROM SalesOrderDetail b WHERE a.SalesOrderID=b.ProductID)

  AND NOT EXISTS( SELECT * FROM SalesOrderDetail b WHERE b.ProductID IS NULL)

 

因此,我们可以看到not in需要额外的步骤去处理null值。上述情况是仅仅在SalesOrderDetail表中的ProductId列定义为允许null,如果将SalesOrderHeader的SalesOrderID列也定义为允许null时,会发现SQL Server还需要额外的成本确认SalesOrderID列上是否有null值。此时not in对应的等价not exists形式变为如下所示:

 

--当连接两列定义都允许null时,not in等价的not exists形式

SELECT *

FROM SalesOrderHeadera

WHERE NOT EXISTS(

SELECT *

  FROM SalesOrderDetail b

  WHERE a.SalesOrderID = b.ProductID )

    AND NOT EXISTS(( SELECT *

                      FROM SalesOrderDetail b

                      WHERE b.ProductIDIS NULL

                      ) )

    AND NOT EXISTS( SELECT 1

                    FROM ( SELECT *

                            FROM SalesOrderHeader

                          )AS c

                    WHERE c.SalesOrderID IS NULL )

 

小结

本文阐述了not in的实现原理以及所带来的数据不一致和性能问题,在写查询时,尽量避免使用not in,而转换为本文提供的not exists等价形式,将会减少很多麻烦。


最后修改时间:2021-04-29 22:49:03
文章转载自SQLServer爱好者,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论