MySQL 5.7及更早版本中,IN子查询易触发嵌套循环、索引估算失准、NOT IN的NULL陷阱及缺失索引等问题,导致性能骤降与逻辑错误;优化需结合索引补全、改用NOT EXISTS/LEFT JOIN、合理调整eq_range_index_dive_limit等。

IN子查询触发嵌套循环执行模型
MySQL 5.7 及更早版本对相关子查询(即子查询里引用了外层表字段)默认采用嵌套循环策略:外层每扫描一行,就完整执行一次子查询。比如 SELECT * FROM users WHERE id IN (SELECT user_id FROM logs WHERE logs.user_id = users.id AND time > NOW() - INTERVAL 7 DAY),若 users 表有 10 万行,logs 表没索引,就可能产生 10 万次全表扫描 logs —— 实测耗时从 0.2 秒飙升到 48 秒。
即使是非相关子查询(不依赖外层),MySQL 也可能先物化为临时表,再与外层做哈希匹配;一旦临时表过大(超过 tmp_table_size),就会落盘,引发 I/O 瓶颈。
优化器在IN列表过大时放弃精确成本估算
当 IN 后面的常量值数量 ≥ eq_range_index_dive_limit(默认 200),MySQL 就不再逐个“下潜”索引树统计范围行数,而是改用粗略的索引统计值(index statistics)估算成本。这极易导致选错执行计划——比如该走 ref 却选了 ALL,该用联合索引却只用了单列索引。
- 现象:EXPLAIN 显示
key为空、rows严重高估、Extra出现Using temporary; Using filesort - 验证方式:
SHOW VARIABLES LIKE 'eq_range_index_dive_limit';,可临时调高(如设为 1000)观察执行计划是否改善 - 注意:调高该值会增加硬解析开销,不能无限制放大
NOT IN 遇 NULL 导致逻辑错误+性能双崩
NOT IN 是三值逻辑陷阱区:只要子查询结果中任意一行是 NULL(例如 SELECT user_id FROM logs WHERE status IS NULL),整个 NOT IN 条件恒为 UNKNOWN,结果集直接为空——业务上查不到任何数据,但 EXPLAIN 看不出异常,排查极其隐蔽。
更糟的是,为判断是否存在 NULL,MySQL 常被迫扫完整个子查询结果集,无法短路,进一步拖慢响应。
- 替代方案必须用
NOT EXISTS或LEFT JOIN ... IS NULL,二者均不受 NULL 影响 - 切勿用
WHERE col NOT IN (subquery) AND subquery_col IS NOT NULL补救——语义已改变,且仍无法规避全量扫描
子查询字段缺失索引,JOIN也救不了
把 IN 改成 JOIN 不等于自动变快。如果子查询涉及的关联字段没索引(比如 logs.user_id 无索引),JOIN 会退化为嵌套循环,和原 IN 子查询性能几乎一致,甚至更差(因多了一次表连接开销)。
必须同步检查并补全索引:
- 对非相关子查询:
EXPLAIN SELECT id FROM customers WHERE status = 'active',确认key列非空 - 对关联字段:
SHOW INDEX FROM logs查看user_id是否有单列或联合索引;若需加过滤条件(如time > ?),优先建联合索引INDEX(user_id, time) - 临时表方案中,
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY)的PRIMARY KEY是强制索引,不可省略
真正卡住的地方往往不是语法怎么写,而是执行计划里那个没被用上的索引,或者子查询里那行被忽略的 NULL。上线前不跑一遍 EXPLAIN,等于蒙眼调优。



















