NOT IN性能差的核心原因是遇NULL返回UNKNOWN,导致优化器放弃索引、退化为嵌套循环全量比对;LEFT JOIN+IS NULL通过显式排除NULL、将过滤条件移至ON子句,绕过三值逻辑陷阱,语义清晰且可控。

NOT IN 在 MySQL 中性能差,核心原因不是写法本身,而是它遇到 NULL 就返回 UNKNOWN,导致优化器放弃索引、退化为嵌套循环全量比对。
NOT IN 遇到 NULL 时为什么直接失效
只要子查询里任意一行的关联字段是 NULL,整个 NOT IN 表达式就恒为 UNKNOWN,最终结果集为空——这符合 SQL 标准,但和业务预期完全不符。更关键的是,MySQL 优化器看到可能含 NULL 的子查询,会主动放弃走索引,转而对主表每行都执行全量扫描右表再逐个比对。
-
SELECT * FROM orders WHERE customer_id NOT IN (SELECT id FROM customers)—— 若customers.id有NULL,这条语句查不到任何数据 - 即使
customers.id加了索引,EXPLAIN中type常为ALL或index,rows接近主表总行数 -
key_len明显偏小,说明索引只用了前缀,后缀字段没参与过滤
LEFT JOIN + IS NULL 为什么能绕过 NULL 陷阱
它把“不在集合中”明确拆成两步:先关联,再判断是否没匹配上。语义清晰,且不依赖三值逻辑。只要你在 ON 子句里提前排除 NULL,就能确保行为可控。
- 必须把原子查询的过滤条件(如
status = 'inactive')移到ON里,不能留在WHERE—— 否则LEFT JOIN会被强制转成INNER JOIN - 右表关联字段(如
c.id)最好声明为NOT NULL;若不能改表,就在ON中加AND c.id IS NOT NULL -
WHERE子句只能写c.id IS NULL,写c.id = NULL永远不成立 - 示例:
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 也不快
它不是银弹。性能取决于右表数据特征和索引质量,盲目替换反而更慢。
- 右表结果极小(比如就 3~5 行),MySQL 可能把
NOT IN展开为常量判断,比JOIN更轻量 - 右表关联字段没索引,
LEFT JOIN会触发全表扫描 + 临时表,比原NOT IN还差 - 原查询带
DISTINCT或聚合,LEFT JOIN可能因一对多产生重复行,需补GROUP BY或DISTINCT,增加开销 - MySQL 5.6 及更早版本对
LEFT JOIN ... IS NULL的索引选择不稳定,务必用EXPLAIN看type是否为ref或range
验证优化是否真生效的三个硬动作
改完不能只看 SQL 顺眼,得靠数据说话:
- 用
EXPLAIN FORMAT=TREE(MySQL 8.0+)或基础EXPLAIN,确认右表走了索引,且没有Using temporary或Using filesort - 在测试库用
SELECT SQL_NO_CACHE跑两遍,对比NOT IN和LEFT JOIN版本的真实执行时间 - 检查结果行数是否一致——特别是右表含
NULL时,NOT IN返回空,而LEFT JOIN不受影响,这点恰恰是优势,但也意味着语义已不同
真正卡住性能的往往不是语法本身,而是连接字段有没有索引、NULL 是否被显式处理、以及过滤条件放在 ON 还是 WHERE 里——这些细节一错,优化就白做。










