SQL实战经验 - SQL查询中关于NULL的4个陷阱
· 阅读需 13 分钟
Copyright © 2023 PawSQL
NULL值处理是应用开发人员最容易出错误的地方,主要的原因是大家习惯使用二元的布尔逻辑来思考判断,而数据库对于NULL值的处理逻辑是三值逻辑。事实上,数据库优化器中缺陷最多的其实也是和NULL值处理相关的逻辑。即使是有着几十年历史的DB2/Teradata等成熟的数据库软件,仍然有超过20%的缺陷和NULL处理相关。
本文深度解析NULL值陷阱出现的根本原因,总结了简单有效的判断逻辑;同时针对日常开发中四种常见陷阱,解释其适用条件及解决方案;最后介绍了PawSQL中和NULL值处理相关的优化规则及其实现原理。看完本篇文章,解决关于NULL值处理的所有疑问。
先说结论
下面的判断逻辑覆盖关系数据库中对于NULL值的所有场景,理解下面的逻辑,你就可以避免NULL陷阱。
- NULL参与的所有的比较和算术运算符(>,=,<,!=,<=,>=,+,-,*,/)结果为unknown
- unknown的逻辑运算(AND、OR、NOT)遵循三值运算的真值表
- 如果运算结果直接返回用户,使用NULL来标识unknown
- 如果运算结果是作为条件判断真假,那么需要通过三值逻辑进行运算,并最终通过以下映射逻辑确定整体判定
- {false、unknown} -> false
- {true} ->true
- 在UNION 或 INTERSECT等集合运算中,NULL 被 视为彼此相等。
三值逻辑
在逻辑学中的三值逻辑(three-valued,也称为三元,或三价逻辑,有时缩写为3VL)是几个多值逻辑系统中的其中之一。有三种状态来表示真、假和一个表示不确定的第三值;这相对于基础的二元逻辑(比如布尔逻辑,它只提供真假两种状态)。
三值逻辑有三个真值(true、false、unknown),它的AND、OR、NOT运算的真值表如下:

关于NULL的4个陷阱
1. 比较谓词与NULL
- 结论:=null并不能判断表达式为空, 判断表达式为空应该使用is null
假设有一张客户表,记录客户的编号,姓名和电话,其中电话和国家代码可以为空。
CREATE TABLE customer (
c_custkey int4 NOT NULL,
c_name varchar(25) NOT NULL,
c_nationcode char(8) NULL,
c_phone varchar(15) NULL,
c_regdate date NULL,
CONSTRAINT customer_pkey PRIMARY KEY (c_custkey)
);
insert into customer values(1, 'Randy', 'en', '13910010010', '20210911');
insert into customer values(2, 'Mandy', null, '13910010012', '20200211');
insert into customer values(3, 'Ray', 'us', null, '20180902');
通过下面的语句获取电话为空的客户是得不到的,它的结果永远为空
select * from customer where c_phone = null;
正确的写法应该是:
select * from customer where c_phone is null;
原因是:根据第一章节里的结论第一条c_phone = null的结果是unknown;然后根据结论里的4.1条unknown判断真假为false.
c_phone=null -> unknown -> false;
2. Case When与NULL
- 结论:
case expr when nulll并不能判断表达式为空, 判断表达式为空应该case when expr is null在where/having的筛选条件的错误写法还比较容易发现并纠正,而在藏在case 语句里使用null值判断就比较难以被发现,一般的SQL审核工具也难以处理。
譬如我们想要获取客户名称和国家的名称, 如果为空,默认是China:
select c_name, case c_nationcode
when 'us' then 'USA'
when 'cn' then 'China'
when null then 'China'
else 'Others' end
from customer
