NOT IN 遇 NULL 返回空结果是 SQL-92 三值逻辑所致:子查询含 NULL 时,id != NULL 恒为 UNKNOWN,导致整个 AND 条件为 UNKNOWN 而被 WHERE 过滤;改用 NOT EXISTS 或显式过滤 NULL(WHERE col IS NOT NULL)可规避。

NOT IN 子查询遇到 NULL 就返回空结果,不是数据库出错,是 SQL-92 标准定义的三值逻辑在严格执行:只要子查询结果里有一个 NULL,整个 WHERE id NOT IN (subquery) 条件就求值为 UNKNOWN,而 WHERE 只保留 TRUE 行,UNKNOWN 和 FALSE 全部被过滤掉。
NOT IN 实际被展开成 AND 判断,而 id != NULL 永远是 UNKNOWN
语句 WHERE id NOT IN (1, 2, NULL) 在逻辑上等价于:
WHERE id != 1 AND id != 2 AND id != NULL
其中 id != NULL 在 SQL 中永远返回 UNKNOWN(不是 TRUE,也不是 FALSE),而 AND 表达式中只要有一个 UNKNOWN,整体就是 UNKNOWN。哪怕主表有明确不匹配的行(比如 id = 100),它照样不会出现。
- 子查询只返回一个
NULL,就足以让整条语句“静默失效” - 常见触发点:LEFT JOIN 后取右表字段(没匹配时为
NULL)、子查询字段未加NOT NULL约束、显式写了UNION SELECT NULL
改用 NOT EXISTS 是最稳妥的替代方案
NOT EXISTS 不做值比较,只判断子查询是否「返回至少一行」,所以 NULL 值不影响行是否存在,天然免疫 NULL 干扰。但它必须写成相关子查询,且关联条件不能漏。
- 错误写法(常量子查询,无关联):
NOT EXISTS (SELECT 1 FROM orders WHERE customer_id = 101) - 正确写法(带外层引用):
NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id) - 原
NOT IN子查询里的其他条件(如status = 'failed')必须一并挪进子查询的WHERE中,否则语义偏移 - 子查询里用
SELECT 1就够了,别写SELECT *或具体字段
LEFT JOIN + IS NULL 方案容易在 ON 和 WHERE 位置上出错
这个方案把「不在集合中」转译为「左连接失败」,语义直观,但极易因条件位置错误导致逻辑变形:
- ON 条件写错(比如漏掉表别名,写成
o.user_id = user_id而非o.user_id = u.id),可能触发全表扫描 - 把本该在
ON里的关联条件错放到WHERE,会导致LEFT JOIN变成INNER JOIN,丢失预期语义 - 右表有重复匹配行时,会产生笛卡尔膨胀;此时
NOT EXISTS更安全
正确示例:SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.user_id IS NULL
如果非要保留 NOT IN,只能显式过滤子查询中的 NULL
唯一安全的补救方式是在子查询中加 WHERE col IS NOT NULL:
SELECT * FROM departments WHERE deptno NOT IN ( SELECT deptno FROM employees WHERE deptno IS NOT NULL );
但这会改变原始语义:
- 你本想排除所有匹配值(含
NULL),现在只排除非空匹配值 - 若业务上
NULL有明确含义(比如“未知部门”),硬过滤可能掩盖数据质量问题 - 子查询若本身返回空集(0 行),
NOT IN反而会返回全部主表数据——这个反直觉行为和NULL无关,但常被一起误判
真正容易被忽略的点不是语法怎么写,而是:改用 NOT EXISTS 后仍出错,往往是因为忘了把原子查询里的过滤条件(比如时间范围、状态码)一并挪进子查询的 WHERE 中。

















