not in几乎必然不走索引,因其破坏b+树有序性与范围定位能力,无法导航子树,遇null语义失效,且受列表大小、类型转换、联合索引前缀等影响;应改用left join+is null等可定位逻辑。

NOT IN 几乎必然不走索引,这不是配置或写法问题,而是 B+ 树索引结构本身无法支持“离散排除”逻辑。
NOT IN 为什么破坏 B+ 树的导航能力
B+ 树靠有序性做范围跳转和等值定位,但 NOT IN (1,5,9,12) 表达的是“所有不等于这四个值的记录”,它不构成连续区间,也没有起始/终止边界。优化器没法从根节点往下导航到某个子树并跳过——必须逐行比对是否“不在列表中”。这种操作天然无法利用树的分层结构,只能退化为全表扫描或全索引扫描。
即使字段有主键索引、唯一索引或联合索引前缀,NOT IN 依然不会触发 range 或 ref 访问类型,EXPLAIN 中 key 字段常为 NULL,type 为 ALL 或 index。
NULL 是 NOT IN 的致命陷阱
只要子查询返回结果里含任意一个 NULL,整个 NOT IN 表达式结果就是 UNKNOWN,WHERE 条件不成立,最终查不到任何数据——哪怕逻辑上该有结果。
-
SELECT * FROM user WHERE id NOT IN (SELECT manager_id FROM dept):只要dept.manager_id有一行是NULL,整条查询结果为空 - 优化器知道这点,所以干脆放弃索引下推,避免在运行时才发现语义失效
- 即便你手动过滤掉
NULL(如WHERE manager_id IS NOT NULL),若该条件没写进子查询的ON或WHERE子句,优化器仍可能忽略
哪些写法会让 NOT IN 显式失效
除了语义和结构限制,以下实操细节会直接让优化器放弃索引:
- 右侧列表过大(比如超过 300 个值):优化器倾向改用哈希反查,但依然不走 B+ 树索引
- 字段类型隐式转换:
id INT列却写NOT IN ('1','2','3'),触发字符串转数字,索引失效 - 联合索引未满足最左前缀:
INDEX (status, created_at),但写成WHERE status NOT IN (0,1) AND created_at > '2023-01-01',整个索引只用到status的等值部分,后面失效 - 子查询带非关联条件:如
NOT IN (SELECT id FROM log WHERE type = 'error'),这个WHERE无法下推,导致子查询先全量执行再比对
LEFT JOIN + IS NULL 不是万能解,但必须按规则写
替代 NOT IN 最常用的是 LEFT JOIN ... IS NULL,但它不是简单替换就能生效:
- 右表连接字段(如
b.user_id)必须有索引,且最好是NOT NULL;若不能改表,得在ON里显式加AND b.user_id IS NOT NULL - 原子查询中的过滤条件(如
status = 'inactive')必须挪进ON子句,不能留在WHERE,否则外连接变内连接 -
WHERE只能写b.user_id IS NULL,写成= NULL永远不成立 - 如果右表结果极少(比如就 3 行),MySQL 5.7+ 有时反而把
NOT IN展开为常量判断,比JOIN更快
真正难处理的,是那些业务上必须表达“排除集合”的场景——这时候得靠预计算白名单 ID 改用 IN,或者引入缓存层提前筛掉无效值。B+ 树索引不支持反向枚举,这是结构性限制,不是调优能绕过去的。











