not in 查询慢是因为遇 null 返回 unknown 导致结果为空且优化器放弃索引下推,易退化为全表扫描;应改用 left join + is null,确保右表连接字段非空、有索引,并将子查询条件移至 on 子句。

NOT IN 查询慢,不是因为语法本身“不高效”,而是它在 MySQL 中极易触发全表扫描、忽略索引,且语义上对 NULL 极其敏感——只要子查询结果里有一个 NULL,整个条件就变成 UNKNOWN,最终查不到任何数据。这不是 bug,是 SQL 标准行为,但线上查不出数据+慢得离谱,往往就是它在作祟。
为什么 NOT IN 会彻底失效并变慢
核心问题不在“不等于”,而在 NULL 和执行计划脱节:
-
SELECT * FROM t1 WHERE id NOT IN (SELECT id FROM t2)—— 若t2.id含NULL,整条WHERE判定为UNKNOWN,结果集为空 - 优化器不敢把索引下推到子查询中,常退化成嵌套循环 + 全表扫描,
EXPLAIN显示type: ALL、key_len: NULL、Extra: Using where; Using join buffer - 即使
t2.id有索引,MySQL 5.7+ 以前版本也几乎从不走索引;5.7+ 虽支持部分优化,但遇到NULL或复杂WHERE条件时仍大概率放弃 - 子查询若带过滤(如
WHERE status = 'active'),NOT IN无法将该条件下推,只能先全量取子结果再逐行比对
LEFT JOIN ... IS NULL 的正确写法和关键约束
改写目标:把“左表某字段值不在右表集合中”转为“左表保留,右表无匹配项”。但必须满足几个硬性前提,否则逻辑错或性能更差:
- 连接字段必须是确定的等值,如
ON a.ref_id = b.id,不能加函数、类型转换或表达式 - 右表用于连接的字段(如
b.id)**必须非空**:要么建表时加NOT NULL约束,要么在ON子句中显式排除:ON a.ref_id = b.id AND b.id IS NOT NULL - 原子查询中的过滤条件(如
status = 'inactive')**必须写进ON,不能放WHERE**,否则外连接变内连接,漏数据 -
WHERE子句中必须用IS NULL,写成= NULL永远不成立
示例对比:
/* 原始慢查询 */ SELECT * FROM orders WHERE customer_id NOT IN ( SELECT id FROM customers WHERE status = 'inactive' );
/* 正确改写 */ SELECT o.* FROM orders o LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'inactive' WHERE c.id IS NULL;
哪些情况 LEFT JOIN 也未必快,得换方案
不是所有 NOT IN 都适合一刀切改成 LEFT JOIN:
- 右表极小(比如几十行)、且无
NULL,NOT IN可能被优化器展开为常量数组,反而比JOIN更轻量 - 右表连接字段没索引,
LEFT JOIN会驱动左表去扫右表全表,还可能生成临时表,比原来更卡 - 右表数据量远大于左表(比如千万级用户表查几百条订单),
LEFT JOIN容易选错驱动表,此时NOT EXISTS更稳:它天然以左表为外层,支持半连接优化(semijoin),且子查询里用SELECT 1即可 - 原查询含
DISTINCT或聚合,强行JOIN可能因一对多产生重复行,需补GROUP BY或DISTINCT
验证是否真生效,三步不能跳
改完别直接上线,立刻做这三件事:
- 跑
EXPLAIN FORMAT=TREE(MySQL 8.0+)或基础EXPLAIN,确认右表访问类型是ref或range,没有Using temporary或Using filesort - 在测试库用
SELECT SQL_NO_CACHE跑两遍,对比原始和改写版真实耗时——缓存会掩盖问题 - 核对结果行数:尤其注意右表含
NULL时,NOT IN返回空,而LEFT JOIN版本不受影响,这是优势,但也意味着语义已不同,业务是否接受?
真正卡住性能的,从来不是 NOT IN 这个词,而是连接字段有没有索引、NULL 怎么处理、以及你写的 ON 和 WHERE 是否让优化器看懂了你的意图。











