MySQL 5.7 中 NOT IN 子查询因相关性与 NULL 敏感性必然全表扫描;8.0 仅在子查询可物化、无 NULL、索引合理且统计信息准确时才优化为哈希连接或物化表查。

MySQL 5.7 对 NOT IN 子句基本放弃索引优化,8.0 在特定条件下能下推条件、避免全表扫描——但前提是子查询结果集可物化且无 NULL 值干扰。
NOT IN 在 5.7 中为何几乎必然触发全表扫描
5.7 遇到 NOT IN (SELECT ...) 时,只要子查询引用外层字段(即相关子查询),就会标记为 DEPENDENT SUBQUERY,执行策略是“外层每行都重跑一次子查询”,且无法利用索引下推。更关键的是:NOT IN 对 NULL 敏感——只要子查询结果里有一个 NULL,整个表达式就恒为 UNKNOWN,导致优化器直接放弃走索引,退化为临时表 + 全表比对。
- 典型 EXPLAIN 现象:
select_type显示DEPENDENT SUBQUERY,rows列反复出现高估值,key为NULL - 即使子查询本身有
WHERE status = 'active',若status字段没索引或顺序不对,5.7 也不会下推过滤,内层仍全量扫描 - 隐式物化临时表时,若数据量大,还会触发磁盘临时表(
Using temporary; Using filesort)
8.0 的改进依赖三个硬性前提
8.0 并非自动变快,它靠 subquery_materialization_cost_based=ON(默认开启)+ 准确统计信息 + 合理索引结构,才可能把 NOT IN 转成哈希连接或物化表查。但一旦不满足任一前提,行为就退回 5.7 模式。
- 子查询不能含
NULL:必须显式加WHERE col IS NOT NULL,否则优化器仍拒绝物化 - 子查询字段需有高效索引:比如
SELECT id FROM users WHERE status = 'active',得在users(status, id)上建联合索引,否则 ICP 不生效 - 必须执行
ANALYZE TABLE users:若统计信息陈旧,Cost Model 会误判物化代价过高,继续选嵌套循环
用 EXPLAIN FORMAT=TREE 快速验证是否真优化了
5.7 不支持该语法,8.0 下运行 EXPLAIN FORMAT=TREE SELECT ... NOT IN ...,重点看输出中是否出现 MATERIALIZATION 节点和 hash_join。如果没有,说明仍在走老路。
- 正确优化路径示例:
-> Materialize with deduplication (cost=...)→-> Inner hash join (cost=...) - 失败信号:
-> Filter: (t1.id not in (select #1)) (cost=...),且子查询节点仍是DEPENDENT SUBQUERY - 注意:即使看到
MATERIALIZATION,也要检查其 cost 值是否显著低于嵌套循环路径;否则优化器可能只是“形式上物化”,实际仍慢
最容易被忽略的一点:8.0 的 NOT IN 优化对索引定义极其苛刻——子查询的 WHERE 条件字段顺序、是否含 IS NOT NULL、甚至外层字段类型是否与子查询列完全一致(比如 INT vs VARCHAR),都会让物化决策直接失效。这不是配置问题,是查询重写阶段的字面匹配失败。


















