MySQL IN子句有时不走索引?调整eq_range_index_dive_limit参数
MySQL的IN子句不走索引,常因优化器在IN列表长度超过eq_range_index_dive_limit阈值(默认200或10)后改用统计信息估算,误判全表扫描更优。可通过会话级临时调高参数解决,但需注意类型匹配、数据分布等干扰因素。
在排查MySQL慢查询时,经常遇到IN子句明明可以走索引,但优化器却选择了全表扫描的情况。第一反应往往是SQL写错了、索引没建对,但反复检查后却发现都不是。实际上,问题很可能出在MySQL优化器的一个“偷懒”开关上——eq_range_index_dive_limit。这个参数决定了优化器是逐个探测IN值,还是用统计信息估算。一旦IN列表长度超过阈值,优化器就会误判全表扫描更便宜,从而放弃索引路径。

IN子句不走索引,往往不是SQL写错了,而是MySQL优化器主动放弃了range访问路径——eq_range_index_dive_limit就是那个关键开关。
为什么eq_range_index_dive_limit会影响IN是否走索引
MySQL在决定是否对IN使用索引时,会对每个IN值执行一次“index dive”(索引下潜),用于估算匹配行数。这个操作本身很精确,但代价不低——每个值都需要去B+树中遍历一次。因此,MySQL设了一个硬性上限:eq_range_index_dive_limit默认值在5.7中是200,在8.0中是10。一旦IN列表长度超过这个数,优化器就会跳过逐个探测,改用统计信息粗略估算——结果常常误判为“全表扫描更便宜”,于是type=all就出现了。
这并非bug,而是一种取舍:避免小查询被大量index dive拖慢,但代价是大IN列表大概率丢失索引。值得注意的是,这个参数只影响IN、=、BETWEEN等等值/范围条件的索引选择,对LIKE 'abc%'这类前缀扫描完全不起作用。另外,修改后只对新执行的查询生效,已缓存的执行计划不会自动刷新。升级到8.0后要特别注意——默认值从200降到了10,更容易触发退化。
- 该参数只影响
IN、=、BETWEEN等等值/范围条件的索引选择,不影响LIKE 'abc%'这类前缀扫描 - 修改后仅对新执行的查询生效,已缓存的执行计划不会自动刷新
- 8.0中默认值激进(10),比5.7(200)更容易触发退化,升级后要特别注意
如何安全地调整eq_range_index_dive_limit
直接使用SET GLOBAL风险较高,建议按需局部调整。最稳妥的方式是会话级临时调高:SET SESSION eq_range_index_dive_limit = 500;,之后再执行IN查询,观察效果。注意不要盲目设置过大(例如10000),否则单次查询可能卡住,尤其当IN值对应索引碎片严重时——index dive成本会飙升。配合ANALYZE TABLE一起使用也很重要,统计信息过期的话,调高该值也无济于事。查询当前值可以使用SELECT @@session.eq_range_index_dive_limit;。
- 会话级临时调高(推荐):
SET SESSION eq_range_index_dive_limit = 500;,之后再执行IN查询 - 不要盲目设置过大(如10000),否则单次查询可能卡住,尤其当
IN值对应索引碎片严重时 - 配合
ANALYZE TABLE一起使用,确保统计信息不过期——失真的基数会让调高该值也无效 - 查询当前值:
SELECT @@session.eq_range_index_dive_limit;
eq_range_index_dive_limit之外,IN不走索引的常见干扰项
不要只盯着这个参数,以下问题会让调高该值也无济于事:
IN里混入了NULL,例如WHERE status IN (1, 2, NULL)→ 优化器直接剪枝索引路径- 字段类型与
IN值类型不一致,比如user_id INT却写IN ('1001', '1002')→ 隐式转换导致索引失效 - 复合索引下跳过了最左列,如索引是
(a,b,c),但写了WHERE b IN (1,2) AND c = 3→ 根本用不到索引 IN值在索引中分布极偏(比如95%都是同一个值),优化器计算后认为回表成本太高,宁可全表扫描
真正要确保IN走索引,需先确认EXPLAIN中type为range、key有具体索引名、rows预估合理;参数只是辅助手段,类型匹配、数据分布、索引设计才是根本。
