NOT IN 遇 NULL 即失效,因表达式整体返回 UNKNOWN 被 WHERE 过滤,结果恒为空;应改用 NOT EXISTS 或 LEFT JOIN + IS NULL,二者语义稳定、可索引优化且不受 NULL 传播影响。

NOT IN 遇到 NULL 就失效,Anti-Join(如 NOT EXISTS 或 LEFT JOIN + IS NULL)不会。
NOT IN 的 NULL 陷阱到底有多致命
只要子查询返回的集合里有一个 NULL,整个 NOT IN 表达式就变成 UNKNOWN,而 WHERE 只接受 TRUE,结果就是——查不到任何行,哪怕左表有 100 万条匹配数据也全被过滤掉。
- 常见错误写法:
SELECT * FROM orders WHERE cust_id NOT IN (SELECT cust_id FROM customers) - 只要
customers.cust_id中存在任意一个NULL,这条语句就永远返回空结果集 - 即使你加了
WHERE cust_id IS NOT NULL在子查询里,优化器也不总能下推该条件,执行计划仍可能走低效路径
NOT EXISTS 是关联子查询,天然规避 NULL 传播
NOT EXISTS 不是集合比较,而是对每一行做“是否存在匹配”的布尔判断,NULL 不会污染整个逻辑。
- 子查询必须含相关列引用,例如:
WHERE o.user_id = u.id - 即使
o.user_id是NULL,它只影响当前这一行的判断,不影响其他行 - 语义稳定:只要右表中没有一行满足关联条件,外层行就保留
- Oracle、PostgreSQL、SQL Server、MySQL 8.0+ 都能将它优化为
HASH JOIN ANTI或NESTED LOOPS ANTI
LEFT JOIN + IS NULL 为什么也安全,但要注意字段选谁
这个写法本质是模拟 Anti-Join,但它依赖你正确选择判空字段——必须是右表的**关联外键字段**,且该字段在业务上允许为 NULL(表示“未匹配”)。
- 正确:
WHERE o.user_id IS NULL(o.user_id是外键,没匹配时为NULL) - 错误:
WHERE o.id IS NULL(o.id是主键,不可能为NULL,永远不成立) - 如果右表关联字段定义为
NOT NULL,那IS NULL永远不成立,整个查询结果为空——这不是 bug,是 schema 设计与查询逻辑不匹配 - MySQL 8.0.18 之前不支持
HASH JOIN ANTI,但至少语义正确;PostgreSQL 和 SQL Server 则大概率能走真正 Anti-Join 计划
真正容易被忽略的是:不是所有写法都能触发优化器的 Anti-Join 转换。哪怕语法正确,如果右表缺少索引、统计信息不准、或子查询里用了 ORDER BY/DISTINCT,NOT EXISTS 也可能退化成逐行 FILTER 扫描。验证唯一办法是看执行计划里有没有 ANTI 字样。

















