首页/数据/SQL 速查/NOT IN 的 NULL 陷阱

NOT IN 的 NULL 陷阱

NOT IN 对包含 NULL 的集合几乎总是返回空结果:因为 x <> NULL 是 UNKNOWN。识别与改写它是排查“查询莫名没数据”的高频问题。

SQL 速查数据更新于 2026-10-13

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 的处理方式。

易错点

  • 排查时先确认子查询列是否有 NULL:SELECT COUNT(*) FROM (SELECT …) x WHERE col IS NULL
  • NOT EXISTS 需要连接键上有索引才不会退化为逐行扫描,改写后用 EXPLAIN 确认
  • 以为把 NOT IN 改成 LEFT JOIN 加 IS NULL 就一定更快,实际要看数据量与索引
  • 内层子查询返回大量数据时性能不升反降,必要时改用临时表先落数据
  • 把 NOT IN 的结果为空当成业务确实没有数据,长期未发现语义 bug

数据来源与参考

  • MySQL 8.0 Reference Manual · Subqueries · NULL Handling
  • PostgreSQL Documentation · Subquery Expressions
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌