NOT IN子句中的NULL值

318

当我认为两个查询是相同的,一个使用not in约束条件,另一个使用left join时,发现它们返回了不同的记录数,这个问题就出现了。在not in约束条件下的表中有一个空值(坏数据),导致该查询返回0条记录的计数。我大致理解了其中的原因,但仍需要一些帮助来完全掌握这个概念。

简单地说,为什么查询A会返回结果而B不会?

A: select 'true' where 3 in (1, 2, 3, null)
B: select 'true' where 3 not in (1, 2, null)

这是在SQL Server 2005上的情况。我还发现调用set ansi_nulls off会导致B返回一个结果。

12个回答

335

查询 A 与以下查询相同:

select 'true' where 3 = 1 or 3 = 2 or 3 = 3 or 3 = null

由于 3 = 3 为真,您将得到一个结果。

查询B与以下内容相同:

select 'true' where 3 <> 1 and 3 <> 2 and 3 <> null

ansi_nulls 开启时,3 <> null 会被认为是UNKNOWN,因此该条件谓词的值也会是UNKNOWN,这样就不会返回任何行。

ansi_nulls 关闭时,3 <> null 会被认为是true,因此该条件谓词的值也会是true,这样就会返回一行记录。


13
有人曾经指出,将NOT IN转换为一系列的<> and会改变“不在这个集合中”的语义行为,变成其他东西了吗? - Ian Boyd
11
看起来在SQL中,"A NOT IN ('X', 'Y')" 实际上是 "A <> 'X' AND A <> 'Y'" 的别名。我看你在https://dev59.com/d2865IYBdhLWcg3wOb5h 中自己发现了这一点,但想确保你的异议在这个问题中得到解决。 - Ryan Olson
4
这是 SQL Server 的一个非常糟糕的行为,因为如果它期望使用 "IS NULL" 进行 NULL 比较,那么它应该将 IN 子句扩展到相同的行为,而不是愚蠢地将错误的语义应用于自身。 - OzrenTkalcecKrznaric
@binki,如果您能更详细地阐述一下您所说的内容,那将会很有帮助。 - Istiaque Ahmed
2
@Ian 不完全是这样。如果你将 null 的含义解释为“某个未知的值”,那么语义就是一致的:3 in (1, 2, 3, unknown) 是真的,因为你_知道_3在这个组中。3 not in (1, 2, unknown) 既不是真的也不是假的,因为它_可能_在这个组中。“给定1、2和我们不知道实际值的东西,3不在这个集合中吗?”我不确定 - Hau
显示剩余6条评论

75

NOT IN对比未知值时返回0条记录

由于NULL是一个未知值,所以在NOT IN查询中包含了NULLNULL在可能值的列表中,始终会返回0条记录,因为无法确定NULL值是否为测试的值。


10
这就是要点答案。即使没有任何例子,我发现这更容易理解。 - Govind Rai

66

每当你使用NULL时,你实际上是在处理三值逻辑。

第一个查询返回结果是因为WHERE子句的求值结果为:

    3 = 1 or 3 = 2 or 3 = 3 or 3 = null
which is:
    FALSE or FALSE or TRUE or UNKNOWN
which evaluates to 
    TRUE
第二个:
    3 <> 1 and 3 <> 2 and 3 <> null
which evaluates to:
    TRUE and TRUE and UNKNOWN
which evaluates to:
    UNKNOWN

未知并不等同于假。 你可以通过调用以下方法轻松测试:

select 'true' where 3 <> null
select 'true' where not (3 <> null)

两个查询都不会返回结果。

如果UNKNOWN等同于FALSE,那么第一个查询将会返回FALSE,第二个查询将会返回TRUE,因为它等同于NOT(FALSE)。
但这并不是事实。

在SqlServerCentral有一篇非常好的关于这个主题的文章

关于NULL和三值逻辑的整个问题可能会有点混乱,但是理解它对于编写正确的TSQL查询是至关重要的。

我还推荐另一篇文章SQL聚合函数和NULL


19

与 null 比较会得到未定义的结果,除非你使用 IS NULL。

所以,当比较 3 和 NULL(查询 A)时,它返回未定义。

例如:SELECT 'true' where 3 in (1,2,null) 和 SELECT 'true' where 3 not in (1,2,null) 将产生相同的结果,因为 NOT (UNDEFINED) 仍然是未定义,而不是 TRUE。


很好的观点。在(ansi)中,选择1 where null in(null)不会返回行。 - crokusek

11

SQL使用三值逻辑进行真值运算。 IN 查询会产生预期结果:

SELECT * FROM (VALUES (1), (2)) AS tbl(col) WHERE col IN (NULL, 1)
-- returns first row

但是添加一个NOT并不会颠倒结果:
SELECT * FROM (VALUES (1), (2)) AS tbl(col) WHERE NOT col IN (NULL, 1)
-- returns zero rows

这是因为上述查询相当于以下内容:
SELECT * FROM (VALUES (1), (2)) AS tbl(col) WHERE NOT (col = NULL OR col = 1)

这是 where 子句的评估方式:

| col | col = NULL⁽¹⁾  | col = 1 | col = NULL OR col = 1 | NOT (col = NULL OR col = 1) |
|-----|----------------|---------|-----------------------|-----------------------------|
| 1   | UNKNOWN        | TRUE    | TRUE                  | FALSE                       |
| 2   | UNKNOWN        | FALSE   | UNKNOWN⁽²⁾            | UNKNOWN⁽³⁾                  |

请注意:

  1. 涉及 NULL 的比较结果为 UNKNOWN
  2. 当没有操作数为 TRUE 且至少有一个操作数为 UNKNOWN 时,OR 表达式的结果为 UNKNOWNref
  3. UNKNOWNNOT 结果为 UNKNOWNref

你可以将上面的示例扩展到多于两个值(例如 NULL、1 和 2),但结果仍然相同:如果其中一个值为 NULL,则不会匹配任何行。


10

如果您想使用NOT IN筛选包含NULL的子查询,请检查是否为空

SELECT blah FROM t WHERE blah NOT IN
        (SELECT someotherBlah FROM t2 WHERE someotherBlah IS NOT NULL )

我遇到了外连接查询的问题,在某些特殊情况下没有返回任何记录,因此我检查了这个解决方案,适用于空记录和存在记录的情况,并且它对我起作用了。如果出现其他问题,我会在这里提到的。非常感谢。 - QMaster
1
+1 我得出了同样的结论,即使我希望有一些内置方法来避免进行这个“IS NOT NULL”检查。 - logi-kal

9
在撰写本问题时,标题为“SQL NOT IN约束和NULL值”。从问题的文本中可以看出,问题发生在SQL DML SELECT查询中,而不是SQL DDL CONSTRAINT。然而,特别是考虑到标题的措辞,我想指出这里有一些可能具有误导性的陈述,比如(引述):“当谓词计算结果为UNKNOWN时,您将不会得到任何行。”尽管对于SQL DML而言是这样,但是考虑到约束条件时效果是不同的。请考虑这个非常简单的表格,其中包含两个约束条件,直接取自问题中的谓词(并由@Brannon提供了一个很好的答案)。
DECLARE @T TABLE 
(
 true CHAR(4) DEFAULT 'true' NOT NULL, 
 CHECK ( 3 IN (1, 2, 3, NULL )), 
 CHECK ( 3 NOT IN (1, 2, NULL ))
);

INSERT INTO @T VALUES ('true');

SELECT COUNT(*) AS tally FROM @T;

根据@Brannon的回答,第一个约束(使用IN)评估为TRUE,第二个约束(使用NOT IN)评估为UNKNOWN。然而,插入操作成功了!因此,在这种情况下,严格来说不能说“你没有得到任何行”,因为我们确实插入了一行作为结果。
以上效果确实是SQL-92标准的正确效果。请比较并对比SQL-92规范中的以下部分:
7.6 WHERE子句
结果是T的那些行的表,其中搜索条件的结果为true。
4.10 完整性约束
仅当表中的任何行的指定搜索条件不为false时,才满足表检查约束。
换句话说:
在SQL DML中,当WHERE评估为UNKNOWN时,会从结果中删除行,因为它不符合条件“为true”。
在SQL DDL中(即约束),当它们评估为UNKNOWN时,不会从结果中删除行,因为它们符合条件“不为false”。
尽管SQL DML和SQL DDL中的效果看起来相互矛盾,但有一个实际的原因可以让UNKNOWN结果获得“得到好处”的怀疑,从而允许它们满足约束(更正确地说,允许它们不失败地满足约束):如果没有这种行为,每个约束都必须显式处理nulls,从语言设计角度来看,这将是非常不令人满意的(更不用说对于程序员来说是一件非常痛苦的事情!)
附注:如果您发现像“unknown不会导致约束失败”这样的逻辑难以理解,就像我写它一样困难,请考虑在SQL DDL中避免可空列和在SQL DML中避免产生nulls(例如外连接)来简化问题!

2
@Jamie Ide:实际上,我对这个问题有另一个答案:因为包含空值的 NOT IN (subquery) 可能会产生意外结果,所以避免使用 IN (subquery) 并总是使用 NOT EXISTS (subquery)(就像我曾经做过的那样!)似乎总是正确处理空值。然而,有些情况下 NOT IN (subquery) 会给出预期结果,而 NOT EXISTS (subquery) 则会给出意外结果!如果我能找到关于这个主题的笔记(需要笔记,因为它不直观!),我可能会写一篇文章。但结论是相同的:避免使用空值! - onedaywhen
1
我认为相反的情况更为真实:我们当前的计算模型饱受双值逻辑(我们只知道如何思考二进制逻辑和二进制值)的文化近视之苦。这个问题在统计学中也经常出现,几乎所有的东西都是未知的。禅宗公案可以成为突破这种近视的好练习。 - DylanYoung
1
考虑以下代码:CREATE TABLE T ( a INT NOT NULL UNIQUE, b INT CHECK( a = b ) ); - 这里的意图是 b 要么等于 a,要么为 null。如果约束条件必须为 TRUE 才能满足,则需要显式处理 nulls,例如 CHECK( a = b OR b IS NULL )。因此,每个约束条件都需要用户为涉及到的每个可空列添加 ...OR IS NULL 逻辑:更复杂,当他们忘记这样做时会出现更多错误等。所以我认为 SQL 标准委员会只是试图实用主义。 - onedaywhen
@DylanYoung:请注意,SQL DML在空值方面并不一致。考虑上表有多行,其中一些但不是所有行在列b中具有空值:(1)SELECT COUNT(b) FROM T返回大于0的数字。(2)SELECT SUM(b) FROM T返回null。我认为委员会考虑了现有的SQL产品如何处理每种情况以及大多数用户会发现什么更有用,并决定不一致。也许他们是勉强这样做的,但这不是疏忽。还有其他需要学习的“特殊情况”! - onedaywhen
糟糕。我现在看到COUNT的不一致性了(我认为与SUM无关)。它将NULL视为不存在!!!这真是个大问题:(有趣的是,它不以相同的方式处理(NULL,NULL)。SUM似乎没问题;在PostgreSQL上,当传递NULL时会出错,这大致相当于返回NULL。我怀疑这是对现实的妥协,正如你所说,但更好的做法是修复不良的编程实践:如果您不想计算NULL,则应明确地从查询中排除它们。 - DylanYoung
显示剩余7条评论

8

在A中,3与集合的每个成员进行相等性测试,产生 (FALSE, FALSE, TRUE, UNKNOWN)。由于其中一个元素为TRUE,条件为TRUE。(这里也可能发生了一些短路操作,因此它实际上会在第一个TRUE出现时停止并且不会评估3=NULL。)

在B中,我认为它将条件评估为NOT(3在(1,2,null)中)。将3与集合进行相等性测试,得到(FALSE, FALSE, UNKNOWN),这被聚合为UNKNOWN。NOT(UNKNOWN)得到UNKNOWN。因此,整个条件的真值为未知,在最后被视为FALSE。


7
从这里的回答可以得出结论,NOT IN (subquery)不能正确处理null,应该使用NOT EXISTS。然而,这样的结论可能过早。在以下情况下,由Chris Date(数据库编程和设计,第2卷第9期,1989年9月)提供的资料表明,NOT IN可以正确处理null并返回正确的结果,而不是NOT EXISTS
考虑一个表sp,表示已知以数量qty供应零件pno的供应商sno。该表目前包含以下值:
      VALUES ('S1', 'P1', NULL), 
             ('S2', 'P1', 200),
             ('S3', 'P1', 1000)

请注意,数量是可为空的,即使不知道数量,也可以记录供应商已知提供零件的事实。
任务是找到已知供应零件号“P1”但不以1000为数量提供的供应商。
以下使用“NOT IN”正确地仅识别供应商“S2”:
WITH sp AS 
     ( SELECT * 
         FROM ( VALUES ( 'S1', 'P1', NULL ), 
                       ( 'S2', 'P1', 200 ),
                       ( 'S3', 'P1', 1000 ) )
              AS T ( sno, pno, qty )
     )
SELECT DISTINCT spx.sno
  FROM sp spx
 WHERE spx.pno = 'P1'
       AND 1000 NOT IN (
                        SELECT spy.qty
                          FROM sp spy
                         WHERE spy.sno = spx.sno
                               AND spy.pno = 'P1'
                       );

然而,以下查询使用相同的一般结构,但使用 NOT EXISTS 时在结果中错误地包括了供应商'S1'(即其数量为null):

WITH sp AS 
     ( SELECT * 
         FROM ( VALUES ( 'S1', 'P1', NULL ), 
                       ( 'S2', 'P1', 200 ),
                       ( 'S3', 'P1', 1000 ) )
              AS T ( sno, pno, qty )
     )
SELECT DISTINCT spx.sno
  FROM sp spx
 WHERE spx.pno = 'P1'
       AND NOT EXISTS (
                       SELECT *
                         FROM sp spy
                        WHERE spy.sno = spx.sno
                              AND spy.pno = 'P1'
                              AND spy.qty = 1000
                      );

因此,NOT EXISTS并不是看起来那么神奇!

当然,问题的根源在于空值的存在,因此“真正”的解决方案是消除这些空值。

可以通过使用两个表之一(可能还有其他设计)来实现:

  • sp,已知供应零件的供应商
  • spq,已知以已知数量供应零件的供应商

请注意,spq 引用 sp 的外键约束可能会更好。

然后可以使用“减”关系运算符(标准 SQL 中的 EXCEPT 关键字)来获得结果,例如:

WITH sp AS 
     ( SELECT * 
         FROM ( VALUES ( 'S1', 'P1' ), 
                       ( 'S2', 'P1' ),
                       ( 'S3', 'P1' ) )
              AS T ( sno, pno )
     ),
     spq AS 
     ( SELECT * 
         FROM ( VALUES ( 'S2', 'P1', 200 ),
                       ( 'S3', 'P1', 1000 ) )
              AS T ( sno, pno, qty )
     )
SELECT sno
  FROM spq
 WHERE pno = 'P1'
EXCEPT 
SELECT sno
  FROM spq
 WHERE pno = 'P1'
       AND qty = 1000;

1
哦,谢谢您真的写了出来……这让我疯狂了。 - Govind Rai

7

Null表示数据缺失,即未知,而不是无数据值。对于来自编程背景的人来说,很容易混淆这一点,因为在C类型语言中,当使用指针时,null确实是什么都没有。

因此,在第一个例子中,3确实在(1,2, null)集合中,因此返回true。

然而,在第二个例子中,您可以将其简化为

select 'true' where 3 not in (null)

因此,没有返回任何内容,因为解析器不知道您要与之进行比较的集合 - 它不是空集,而是未知集合。 使用(1、2、null)也没有帮助,因为(1、2)集合显然是false,但是然后你将其与未知and,这是未知的。


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