not in 遇到 null 返回空结果,因其等价于 id != 1 and id != 2 and id != null,而 id != null 恒为 unknown,导致整个条件为 unknown,被 where 过滤;快速判断方法是查子查询是否存在 null;最稳妥改写为 left join + is null。

NOT IN 遇到 NULL 为什么直接返回空结果
因为 NOT IN 不是简单“排除列表里的值”,而是被展开为一串 AND 条件:比如 id NOT IN (1, 2, NULL) 等价于 id != 1 AND id != 2 AND id != NULL。而 id != NULL 永远是 UNKNOWN(SQL 三值逻辑),整个表达式就变成 TRUE AND TRUE AND UNKNOWN → UNKNOWN。WHERE 只保留 TRUE 行,UNKNOWN 被丢弃,结果就是 0 行。
怎么快速判断是不是 NULL 导致的失效
不用猜,直接查子查询本身是否含 NULL:
- 运行
SELECT COUNT(*) FROM t2 WHERE id IS NULL,只要结果 > 0,NOT IN就必然失效 - 或者用
SELECT id FROM t2 LIMIT 5手动扫一眼,很多脏数据场景里NULL就混在第一屏 - 别依赖
EXPLAIN——它通常不报NULL问题,只显示type: ALL或Extra: Using where; Using join buffer这类间接信号
最稳妥的改写方式:LEFT JOIN + IS NULL
不是所有替代方案都等效。NOT EXISTS 虽好,但写法易错;过滤 NULL 虽简单,但掩盖了数据质量问题。用 LEFT JOIN 是兼顾语义清晰、执行可控、结果可验证的选择:
- 原语句:
SELECT * FROM t1 WHERE id NOT IN (SELECT id FROM t2) - 改写后:
SELECT t1.* FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.id IS NULL - 必须确保
t2.id有索引;若允许NULL,得在ON子句里加AND t2.id IS NOT NULL,否则IS NULL判断会误判 - 如果
t2原本带条件(如WHERE status = 'inactive'),一定要移到ON里,不能留在WHERE,否则外连接变内连接
容易被忽略的类型与字符集陷阱
即使没 NULL,LEFT JOIN 也可能走不了索引,导致性能没改善:
-
t1.id是INT,t2.id是VARCHAR?隐式转换会让索引失效,得统一类型或加CAST(t2.id AS SIGNED) - 两张表字符集不同(比如
utf8mb4vsutf8)?连接字段比较时会触发全表扫描 -
ON里写了函数,比如ON t1.id = UPPER(t2.id)?索引直接作废 - 用
EXPLAIN看key_len是否为预期值、type是否为ref或eq_ref,而不是ALL
真正让 NOT IN 慢下来的,往往不是语法本身,而是 NULL 穿透、类型不一致、索引没落在连接字段上这三件事叠在一起。改写前先查子查询有没有 NULL,改写后立刻 EXPLAIN 对比,比凭经验调优更可靠。











