PostgreSQL NULL 不等于 NULL

FreeGuideOnline 最新 2026-07-04

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 NULLIS NOT NULL 是唯一能正确判断 NULL 的谓词,它们直接返回 TRUE 或 FALSE,不受三值逻辑影响。

4. 进阶场景:NULL 在 DISTINCT、UNIQUE 约束和聚合函数中的行为

DISTINCT 和 GROUP BY 会把所有 NULL 看成“一组”

虽然 NULL = NULL 返回 NULL,但在 SELECT DISTINCTGROUP 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 FROMIS 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,考虑在 WHEREJOINCASE 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 EXISTSLEFT JOIN / IS NULL 方式来避免。

7. 总结

  • NULL 代表未知,任何与 NULL 的比较都返回 NULL(未知),而不是 TRUE/FALSE。
  • 判断 NULL 必须使用 IS NULLIS NOT NULL,切忌 = NULL
  • 从集合角度看,多个 NULL 在 DISTINCT 或 GROUP BY 中被视为一组,但在 UNIQUE 约束中默认看作不同。
  • 善用 IS DISTINCT FROMCOALESCE 等工具来编写安全、清晰的 SQL。

理解了 NULL 的三值逻辑,你不仅能在 PostgreSQL 中写出更健壮的查询,也更接近关系数据库的设计本质。下次再遇到 NULL = NULL 返回 NULL 时,你完全可以自信地给同事解释其中的原理了。


本教程由 [免费在线教程] 网站提供,专注于以清晰易懂的方式讲解数据库核心技术。