in列表超阈值(默认200)导致优化器放弃索引而全表扫描,主因是eq_range_index_dive_limit限制使成本估算失真,并非icp失效;可行方案包括分批查询、临时表join或改写为exists。

IN 列表过大本身不会让“索引下推”(ICP)失效——ICP 是 MySQL 5.6+ 对索引扫描过程中提前过滤的优化机制,它作用于已经选定的索引扫描路径上;真正出问题的是优化器压根没选索引扫描,而是退化为 type: ALL 全表扫描。所以这不是 ICP 失效,是索引根本没被用上。
为什么 IN 超过 200 个值后优化器就放弃走索引?
核心原因是 eq_range_index_dive_limit 默认值为 200。当 IN 列表长度超过该阈值,MySQL 5.7 不再对每个值执行 index dive(即不下潜到索引 B+ 树叶子页统计匹配行数),转而用粗略的全局统计(如 cardinality)估算成本。估算偏差大,常误判“走索引比全表扫描还贵”,于是跳过索引,直接选 type: ALL。
常见现象:
-
EXPLAIN中possible_keys显示有索引,但key为NULL -
Extra字段为空或仅含Using where,无Using index condition - 即使
store_id和activated_time都建了索引,查询仍扫几千万行
IN 参数过多时,哪些写法会雪上加霜?
不是所有“多值 IN”都等价,以下写法会让优化器更难决策,加速退化:
- 混合类型:例如
store_id IN ('1', '2', 3)—— 字符串和整数混用,触发隐式转换,索引列被函数包裹,直接失效 - 子查询未物化:
WHERE id IN (SELECT id FROM t2 WHERE ...)—— MySQL 5.7 默认不物化,先扫t2再逐行匹配主表,外层无法利用索引定位 - 同时带范围 + 大 IN:
WHERE activated_time BETWEEN ? AND ? AND store_id IN (2000+ 个值)—— 优化器可能放弃复合索引的范围部分,只尝试单字段索引,而单字段又因 IN 过长被弃用
临时调高 eq_range_index_dive_limit 真的有用吗?
不推荐。把 eq_range_index_dive_limit 从 200 改成 1000,看似能让大 IN 继续走 index dive,但代价明显:
- 每次查询要访问最多 1000 个索引页做 dive,单次延迟飙升
- 高并发下 IOPS 暴涨,容易打满磁盘,引发连锁超时
- 该变量是会话级或全局级,改了会影响所有查询,不是定向修复
- MySQL 8.0+ 也未取消该限制,只是增加了物化提示(
/*+ MATERIALIZE */),但默认不生效
真正能落地的三种替代方案
优先按稳定性排序:
-
分批查:把 5000 个 ID 拆成每批 ≤ 200 个,应用层循环执行。注意:若原语句带
LIMIT 10,不能简单每批LIMIT 10后拼UNION ALL,必须合并结果后再取 Top-N -
临时表 + JOIN:先
CREATE TEMPORARY TABLE tmp_ids (id INT PRIMARY KEY),批量INSERT所有 ID,再JOIN all_imei_info ON a.store_id = tmp_ids.id。MySQL 对临时表统计信息更准,且能稳定走type: eq_ref -
改写为 EXISTS:适用于
IN (SELECT ...)场景,例如把WHERE store_id IN (SELECT store_id FROM store_table WHERE is_del = 0)改成WHERE EXISTS (SELECT 1 FROM store_table WHERE store_table.store_id = a.store_id AND is_del = 0)。EXISTS 是半连接,优化器通常保留外层索引
最容易被忽略的是权限与语义一致性:临时表方案要求客户端有 CREATE TEMPORARY TABLES 权限;分批方案中 ORDER BY 和 LIMIT 的语义必须在外层聚合后重算,否则结果错误。











