PostgreSQL NULL 不等于 NULL
PostgreSQL 中为什么 NULL 不等于 NULL?一文彻底搞懂空值比较
对于刚接触数据库的开发者来说,经常会发现一个“反直觉”的现象:在 PostgreSQL 中执行 SELECT NULL = NULL;,返回的结果竟然是 NULL 而不是 TRUE。这背后隐藏着 SQL 标准中关于**空值(NULL)**的核心设计哲学。本教程将从零开始,通过丰富的示例带你深入理解 NULL 的比较逻辑,并掌握在实际开发中正确处理 NULL 的技巧。
1. 什么是 NULL?它真的“等于”空吗?
在进入比较逻辑之前,我们需要先对齐对 NULL 的基本认知。
-
NULL 不是零,也不是空字符串
数值 0 和字符串''都是一个确定的值。而 NULL 表示“未知”、“缺失”或“不适用”的状态。 -
NULL 是一种标记,而非值
可以把 NULL 理解为数据库在某个字段上贴的一个“无数据”标签,它不代表任何具体的数据实体。
正因为 NULL 代表未知,两个未知的东西无法断定它们是否相等。这就好比两个密封的盒子,你不知道里面装的是什么,自然无法回答“这两个盒子里的东西一样吗”这个问题。在 SQL 中,任何与 NULL 进行的比较运算,结果既不是 TRUE 也不是 FALSE,而是另一个 NULL(即未知)。这就是三值逻辑(three-valued logic)的核心。
2. 用三值逻辑理解 NULL 比较
普通比较只有 TRUE 和 FALSE 两种结果。但在引入 NULL 后,逻辑结果就多出了 UNKNOWN。PostgreSQL 使用 NULL 来表示这个 UNKNOWN。
来看一个简单的真值表:
| 表达式 | 逻辑结果 |
|---|---|
1 = 1 |
TRUE |
1 = 2 |
FALSE |
NULL = 1 |
NULL |
NULL = NULL |
NULL |
NULL <> 1 |
NULL |
NULL <> NULL |
NULL |
所有涉及 NULL 的算术运算(如 NULL + 5)和字符串拼接(如 'Hello' || NULL)同样会返回 NULL。这是为了确保“未知”能够正确传播:如果数据是未知的,那么基于该数据的计算结果也应该是未知的。
3. 实战示例:验证 NULL 不自等
跟着下面的步骤在你自己本地的 PostgreSQL 环境中操作,加深印象。
创建测试表并插入数据
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', NULL),
('Charlie', 'charlie@example.com'),
('David', NULL);
直接比较 NULL 的查询陷阱
-- 尝试找出 email 为 NULL 的用户
SELECT * FROM users WHERE email = NULL;
结果:0 行记录。
为什么?因为 email = NULL 的结果永远是 NULL,而 WHERE 子句只保留条件结果为 TRUE 的记录。NULL 不是 TRUE,所以所有行都被过滤掉了。
正确的做法:使用 IS NULL 和 IS NOT NULL
-- 查找 email 为未知的用户
SELECT * FROM users WHERE email IS NULL;
-- 查找 email 已知的用户
SELECT * FROM users WHERE email IS NOT NULL;
IS NULL 和 IS NOT NULL 是唯一能正确判断 NULL 的谓词,它们直接返回 TRUE 或 FALSE,不受三值逻辑影响。
4. 进阶场景:NULL 在 DISTINCT、UNIQUE 约束和聚合函数中的行为
DISTINCT 和 GROUP BY 会把所有 NULL 看成“一组”
虽然 NULL = NULL 返回 NULL,但在 SELECT DISTINCT 或 GROUP BY 中,多个 NULL 会被视为无法区分的同一组。
SELECT DISTINCT email FROM users;
-- 结果只显示一个 NULL 行,而不是两个
同理,在使用聚合函数时:
SELECT email, COUNT(*) FROM users GROUP BY email;
count 会在 NULL 对应的分组中记为实际行数(这里是 2)。这体现了 SQL 标准在集合处理时对 NULL 的特殊合并规则。
聚合函数大多忽略 NULL
除 COUNT(*) 外,大多数聚合函数(如 SUM, AVG, MAX, MIN)在处理列时会自动忽略 NULL 值。
-- 假设有一个 scores 表,包含 NULL
SELECT AVG(score) FROM scores; -- 只对非 NULL 的分数求平均
SELECT COUNT(score) FROM scores; -- 只计数非 NULL 行
SELECT COUNT(*) FROM scores; -- 计数所有行,包含 NULL
UNIQUE 约束允许有多个 NULL
这也是初学者容易困惑的地方:如果给 email 列添加了 UNIQUE 约束,你仍然可以插入多条 email 为 NULL 的记录。原因还是:NULL 被视为互不相等,所以每个 NULL 都看作与其他值(包括其他 NULL)不同。PostgreSQL 遵循此标准行为(除非使用 NULLS NOT DISTINCT 语法,这点后文会提及)。
5. NULL 安全的比较运算符:IS DISTINCT FROM
从 PostgreSQL 8.0 开始,提供了 IS DISTINCT FROM 和 IS NOT DISTINCT FROM 运算符,它们在比较时会把 NULL 当作“普通值”处理。
| 表达式 | 结果 |
|---|---|
NULL IS DISTINCT FROM NULL |
FALSE |
1 IS DISTINCT FROM NULL |
TRUE |
NULL IS NOT DISTINCT FROM NULL |
TRUE |
这两个运算符非常适合在连接条件或需要区分 NULL 的业务逻辑中使用。
应用示例:找出 email 与 'alice@example.com' 不同的用户(但保留 email 为 NULL 的用户)
如果直接写 email <> 'alice@example.com',所有 email 为 NULL 的行都会因为三值逻辑被丢弃。改用:
SELECT * FROM users WHERE email IS DISTINCT FROM 'alice@example.com';
此时,email 为 NULL 的行也会出现在结果中,因为它们确实与给定值不同。
6. 高级建议与最佳实践
-
永远不要使用
= NULL或<> NULL
代码审查中一旦看到这种写法就要立刻修正。养成只用IS NULL/IS NOT NULL的习惯。 -
写查询时预判 NULL 的影响
如果某列允许 NULL,考虑在WHERE、JOIN或CASE WHEN中显式处理 NULL 分支,避免丢失数据。 -
利用 COALESCE 或 NULLIF 将 NULL 转换为可比较的值
COALESCE(email, '')可以把 NULL 临时替换为空字符串,NULLIF则相反。这些函数让逻辑更可控。 -
PostgreSQL 15 之后的 NULLS NOT DISTINCT
从 PG 15 开始,你可以在创建唯一约束或索引时指定NULLS NOT DISTINCT,让多个 NULL 也视为重复并触发约束报错。这适应了某些业务场景的需求。
CREATE UNIQUE INDEX idx_users_email ON users (email) NULLS NOT DISTINCT;
- 理解 NULL 在 NOT IN 子查询中的致命陷阱
如果子查询结果集中包含 NULL,NOT IN整个条件可能返回空集。这是老生常谈的踩坑点,根源仍是三值逻辑。建议改用NOT EXISTS或LEFT JOIN / IS NULL方式来避免。
7. 总结
- NULL 代表未知,任何与 NULL 的比较都返回 NULL(未知),而不是 TRUE/FALSE。
- 判断 NULL 必须使用
IS NULL或IS NOT NULL,切忌= NULL。 - 从集合角度看,多个 NULL 在 DISTINCT 或 GROUP BY 中被视为一组,但在 UNIQUE 约束中默认看作不同。
- 善用
IS DISTINCT FROM和COALESCE等工具来编写安全、清晰的 SQL。
理解了 NULL 的三值逻辑,你不仅能在 PostgreSQL 中写出更健壮的查询,也更接近关系数据库的设计本质。下次再遇到 NULL = NULL 返回 NULL 时,你完全可以自信地给同事解释其中的原理了。
本教程由 [免费在线教程] 网站提供,专注于以清晰易懂的方式讲解数据库核心技术。