not in/not exists 的 null 陷阱

简介: 以前遇到了 not in 子查询的一个 null 陷阱,有经验的朋友可能知道怎么回事了,用代码简单说明一下:-- 创建两张测试表:create table tmp01 as wi...

以前遇到了 not in 子查询的一个 null 陷阱,有经验的朋友可能知道怎么回事了,用代码简单说明一下:

-- 创建两张测试表:
create table tmp01 as 
with tmp as (
  select 1 as id from dual  union all
  select 2       from dual  union all
  select 3       from dual  union all
  select null    from dual
)
select * from tmp;

create table tmp02 as 
with tmp as (
  select 1 as id from dual  union all
  select 2       from dual  union all
  select null    from dual
)
select * from tmp;

我现在想知道表tmp01有哪些id值不在tmp02中,于是我随手就写了一条语句:

select id
from tmp01 
where id not in ( select id from tmp02 )

我期望的结果是:

         ID
----------
         3

但实际结果却是:

no rows selected近日读到了dinjun123的大作《符合列NULL问题的研究》,终于静下心来想想这个问题。



通常使用 not in / not exists 的场景是希望得到两个集合的“差集”,与真正的差集又略有不同,后文将会提到,一般的写法有两种:

select id from tmp01 where id not in ( select id from tmp02 )
select id from tmp01 where not exists ( select 1 from tmp02 where tmp02.id=tmp01.id )

正如上文提到的例子,第一条语句没有可返回的行(no rows selected),第二条语句返回了结果是:

         ID
----------
    (null)
         3

为什么第一个没有结果呢?

我们可以将第一条语句重写为:

select id from tmp01 where id<>1 and id<>2 and id<>null

id=1或者2的时候很好理解,当id=3的时候,id<>null 的判断结果是UNKNOW,注意不是false,where子句只认true,其他都不认,所以tmp01中没有一个值经过 id<>1 and id<>2 and id<>null 这个长长的条件判断后能获得true,也就不会有结果集返回了。

那第二条语句为什么返回的结果是两条呢?3容易理解,null为什么也在结果集中呢?明明tmp02中有null值的啊,我们仔细看一下子查询的where 子句 tmp02.id=tmp01.id,我们再逐个值来跟踪一下,这里我用笛卡尔乘积来获得结果:

set pagesize 6;
select 
  tmp01.id "tmp01.id" , 
  tmp02.id "tmp02.id" , 
(select case when count(*)>0 
             then '   Yes         ' 
             else '   No          ' 
             end  from dual where tmp01.id=tmp02.id) "Result Exists?" 
from tmp01,tmp02
order by 1,2

结果如下:

  tmp01.id   tmp02.id Result Exists?
---------- ---------- ---------------
         1          1    Yes
         1          2    No
         1      (null)   No

  tmp01.id   tmp02.id Result Exists?
---------- ---------- ---------------
         2          1    No
         2          2    Yes
         2     (null)    No

  tmp01.id   tmp02.id Result Exists?
---------- ---------- ---------------
         3          1    No
         3          2    No
         3     (null)    No

  tmp01.id   tmp02.id Result Exists?
---------- ---------- ---------------
    (null)          1    No
    (null)          2    No
    (null)     (null)    No

从结果来看有这么一个规律:只要 null 参与了比较,Result Exists? 就一定为NO(因为结果是UNKNOW),这个也是关于 null 的基本知识,这就解析了为什么第二条语句的输出是两行。 



从上面的分析,我们可以“窥视”出 in/not in 的结果是依赖于“=”等值判断的结果;exists/not exists 虽然是判断集合是否为空,但通常里面的子查询做的是值判断。

知道了造成结果集出乎意料的原因,我们就可以修改我们的SQL了,为了测试方便,将原来的表tmp01和tmp02改名:

rename tmp01 to tmp01_with_null;
rename tmp02 to tmp02_with_null;

我们看看测试用例:

test case id   tmp01 has null   tmp01 has null  result has null
------------- ---------------- ---------------- ----------------
            1             true             true            false
            2             true            false             true
            3            false             true            false
            4            false            false            false

其中test case 4 就是打酱油的,只要SQL没有写错,一般不会出问题。

最终,SQL语句改写为:

-- not in 求差集
with tmp01 as (
  select id from tmp01_with_null --where id is not null
),
tmp02 as (
  select id from tmp02_with_null --where id is not null 
)
-- start here
select id from tmp01 
where id not in ( select id from tmp02 where id is not null )
-- 以下是新加的,应付 test case 2
union all
select null from dual 
where exists ( select 1 from tmp01 where id is null )
and not exists ( select 1 from tmp02 where id is null )

-- not exists 求差集
with tmp01 as (
  select id from tmp01_with_null --where id is not null
),
tmp02 as (
  select id from tmp02_with_null --where id is not null 
)
-- start here
select id from tmp01 
where not exists ( 
  select 1 from tmp02 
  where (tmp02.id=tmp01.id) 
  -- 这行是新加的,应付 test case 1
  or (tmp02.id is null and tmp01.id is null )  
)

写了这么多,有人会提议使用minus操作符:

with tmp01 as (
  select id from tmp01_with_null --where id is not null
),
tmp02 as (
  select id from tmp02_with_null --where id is not null 
)
-- start here 
select id from tmp01
minus
select id from tmp02 

貌似语句很简单,但是结果确不一样,请看下面这条语句:

with tmp01 as (
  select id from tmp01_with_null --where id is not null
  union all                      -- 注意这里,现在tmp01已经有重复行了
  select id from tmp01_with_null -- 注意这里,现在tmp01已经有重复行了
),
tmp02 as (
  select id from tmp02_with_null --where id is not null 
)
-- start here 
select 'minus ' as sql_op,id from tmp01
minus
select 'minus ',id from tmp02 
union all
-- not in
select 'not in',id from tmp01 
where id not in ( select id from tmp02 where id is not null )
union all
select 'not in',null from dual 
where exists ( select 1 from tmp01 where id is null )
and not exists ( select 1 from tmp02 where id is null )
union all
-- not exists
select 'not exists',id from tmp01 
where not exists ( 
  select 1 from tmp02 
  where (tmp02.id=tmp01.id) 
  -- 这行是新加的,应付 test case 1
  or (tmp02.id is null and tmp01.id is null )  
);SQL_OP             ID
---------- ----------
minus               3
not in              3
not in              3
not exists          3
not exists          3

minus消灭了重复行!这就是前文所说的 not in 和 not exists 并非真正意义上的差集。



麻烦各位看看有没有硬伤,毕竟乱写东西忽悠人可是很大的罪过啊。
目录
相关文章
|
4月前
|
Java
Error:(15, 13) java: No property named “id” exists in source parameter(s). Did you mean “null”?
Error:(15, 13) java: No property named “id” exists in source parameter(s). Did you mean “null”?
93 1
Error:(15, 13) java: No property named “id” exists in source parameter(s). Did you mean “null”?
Error:(15, 13) java: No property named “id” exists in source parameter(s). Did you mean “null”?
MapStruct - No property named “XXX“ exists in source parameter(s). Did you mean “null“?
MapStruct - No property named “XXX“ exists in source parameter(s). Did you mean “null“?
1295 0
|
Oracle 关系型数据库 MySQL
关于ORACLE MYSQL NOT IN和NOT exists需要注意的 NULL值
首先说明NOT IN 和NOT EXISTS 并不完全等价 ORACLE MYSQL 都是如此 源表: SQL> select * from testa1; NAME                          ID ---------------...
869 0
not exists 与 select null,select 1的作用
1. 构建虚拟表 create table tmp01 as with tmp as ( select 1 as id from dual union all select ...
1014 0
|
4月前
|
SQL 关系型数据库 MySQL
实时计算 Flink版产品使用合集之从MySQL同步数据到Doris时,历史数据时间字段显示为null,而增量数据部分的时间类型字段正常显示的原因是什么
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStreamAPI、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
1月前
|
SQL 关系型数据库 MySQL
在 MySQL 中使用 IS NULL
【8月更文挑战第12天】
590 0
在 MySQL 中使用 IS NULL
|
4月前
|
SQL 关系型数据库 MySQL
python在mysql中插入或者更新null空值
这段代码是Python操作MySQL数据库的示例。它执行SQL查询从表`a_kuakao_school`中选取`id`,`university_id`和`grade`,当`university_id`大于0时按升序排列。然后遍历结果,根据`row[4]`的值决定`grade`是否为`NULL`。若不为空,`grade`被格式化为字符串;否则,设为`NULL`。接着构造UPDATE语句更新`university`表中对应`id`的`grade`值,并提交事务。重要的是,字符串`NULL`不应加引号,否则更新会失败。
129 2
|
1月前
|
SQL 关系型数据库 MySQL
mysql不等于<>取特定值反向条件的时候字段有null值或空值读取不到数据
对于数据库开发的专业人士来说,理解NULL的特性并知道如何正确地在查询中处理它们是非常重要的。以上所介绍的技巧和实例可以帮助你更精准地执行数据库查询,并确保数据的完整性和准确性。在编写代码和设计数据库结构时,牢记这些细节将有助于你避免许多常见的错误,提高数据库应用的质量与性能。
42 0