NOT IN 的 NULL 陷阱是什么
NOT IN 对包含 NULL 的集合几乎总是返回空结果:因为 x <> NULL 求值为 UNKNOWN,而 WHERE 只保留 TRUE 的行。这个陷阱在子查询里更隐蔽——只要子查询返回的列里存在任意一个 NULL,整条语句的结果就为空,且不报任何错。推荐的反连接写法是 NOT EXISTS,它与 NULL 无关,语义也更直白。如果必须用 NOT IN,先在子查询里加 WHERE col IS NOT NULL 把空值排除掉。
基本信息
| 问题根源 | x <> NULL 得到 UNKNOWN,不满足 WHERE |
|---|---|
| 触发条件 | 列表或子查询里出现任意一个 NULL |
| 后果 | 结果集为空且不报错,排查时容易误判为业务无数据 |
| 推荐改写 | NOT EXISTS,或用 LEFT JOIN 加 IS NULL |
| 应急修法 | 子查询里加 WHERE col IS NOT NULL |
| 适用平台 | 三值逻辑是标准行为,MySQL 与 PostgreSQL 表现一致 |
典型使用场景
- 排查查询莫名无结果:条件里出现 NOT IN 时它是第一个怀疑对象
- 求差集:找出 A 表有而 B 表没有的记录,改用 NOT EXISTS
- 黑名单过滤:判断用户不在黑名单里,注意黑名单列可能为空
- 白名单校验:白名单列可空时必须先排除 NULL 再比较
- SQL 评审清单:把 NOT IN 加子查询列为重点复查写法
- 迁移验证:从其他数据库迁到 PostgreSQL 后核对行为一致性
复现与改写
复现与改写
SELECT * FROM t WHERE id NOT IN (1, 2, NULL); # 结果为空
SELECT * FROM t
WHERE id NOT IN (SELECT user_id FROM blacklist);
-- blacklist.user_id 只要有一个 NULL,整条语句结果就为空
-- 改写一:NOT EXISTS,不受 NULL 影响
SELECT * FROM t
WHERE NOT EXISTS (
SELECT 1 FROM blacklist b WHERE b.user_id = t.id
);
-- 改写二:先用 IS NOT NULL 排除空值
SELECT * FROM t
WHERE id NOT IN (SELECT user_id FROM blacklist WHERE user_id IS NOT NULL);反连接写法对照
| 写法 | 含 NULL 时 | 性能提示 |
|---|---|---|
| NOT IN (子查询) | 结果可能整体为空 | 内层列可空时不可用 |
| NOT EXISTS | 语义正确 | 内表连接键要有索引 |
| LEFT JOIN + IS NULL | 语义正确 | 判断列选非空的主键 |
| IN (子查询) | NULL 不影响命中 | 风险较小,命中即返回 |
| EXCEPT / MINUS | 集合运算不受影响 | 部分数据库支持,可读性好 |
子查询列可空时 NOT IN 的语义会被 NULL 破坏,生产 SQL 优先用 NOT EXISTS,或在子查询里先过滤掉 NULL。
NOT EXISTS 需要内表连接键上有索引才不会退化为逐行扫描,改写完成后用 EXPLAIN 确认访问方式变好了。
排查时先确认子查询列是否有 NULL:单独跑一遍 SELECT COUNT(*) FROM (…) x WHERE col IS NULL,几秒钟就能定位是不是这个原因。
NOT EXISTS 与 NOT IN 在结果上一致的前提是内层列非空;数据模型里给关键列加 NOT NULL 约束,能从源头避免这类问题。
排查与改写要点
- 看到 NOT IN 先看内层列是否可空,这是判断风险的第一步
- 内层结果可能很大时用 NOT EXISTS,语义更稳且优化器处理更好
- 改写后对比行数:改前为空、改后有数据,基本可以确认是 NULL 引起的
- LEFT JOIN 加 IS NULL 写法要判断右表非空列,用主键最保险
- 把这条规则写进团队 SQL 规范,评审时统一拦截
- 给可空列补 NOT NULL 约束或默认值,从建模阶段减少隐患
常见误用
- NOT IN 的子查询列可空,结果整体为空且不报错
- 用 NOT IN 求差集,数据量一变结果就变,很晚才发现语义问题
- 把 NOT IN 改成 LEFT JOIN 加 IS NULL 时判断列选错,结果与预期正好相反
- 不看执行计划就改写,内表没有索引时新写法反而更慢
- 对可空列判空时用 <> 而不是 IS NULL,条件永远不成立
- 把空结果当成业务本来就没有数据,长期未发现查询逻辑错误
把这条规则写进团队 SQL 评审清单比事后排查划算:凡出现 NOT IN 加子查询,一律要求作者先确认内层列是否可空。
差集查询还有一种写法是 EXCEPT(PostgreSQL)或左连接加 IS NULL;哪种更合适要看数据量与可读性,但都要先确认 NULL 的处理方式。