in列表超1000项时优化器放弃索引选择,降级为index或all扫描,应优先用join或临时表替代,建索引、批量插入并确保类型与字符集一致。

IN列表超过1000个值时优化器直接放弃索引选择
MySQL优化器对IN常量列表的处理有明确阈值:当值数量超过eq_range_index_dive_limit(默认200)时,它不再深入索引树统计精确行数,转而依赖粗略的索引统计信息做成本估算;一旦超过1000,大概率降级为index或ALL扫描。这不是“慢”,而是执行计划彻底失效。
- 用
EXPLAIN能看到type从range变成index,且rows预估远高于实际 -
max_allowed_packet可能被触发,导致查询直接报错Packets for query is too large - SQL解析阶段开销剧增——每个值都要被词法分析、语法树构建,CPU占用飙升
子查询场景下IN强制物化整个结果集
当IN右侧是子查询(如WHERE id IN (SELECT id FROM t2)),MySQL 5.7及之前版本默认采用物化策略:先执行子查询,把全部结果写入临时表,再与外层逐行比对。几十万行的结果集一落地,就极易突破tmp_table_size,转成磁盘临时表,IO爆炸。
- 监控指标
Created_tmp_disk_tables突增是典型信号 -
NOT IN更危险:子查询结果只要含一个NULL,整个条件恒为FALSE,且优化器会主动放弃索引 -
EXPLAIN中出现Using temporary和Using filesort组合,基本可判定已掉坑
为什么EXISTS能绕过物化,而JOIN更适合取交集
EXISTS本质是半连接,优化器知道“只找一个就停”,所以能下推关联条件、复用索引,不生成中间结果集;JOIN则让优化器有机会选哈希连接或BNL,尤其当内表结果集小但索引好时,性能碾压IN子查询。
- 改写
WHERE id IN (SELECT id FROM t2 WHERE ...)→WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id AND ...),注意必须保留t2.id = t1.id关联条件 - 改写
WHERE id IN (SELECT id FROM t2)→INNER JOIN t2 ON t1.id = t2.id,别用LEFT JOIN ... IS NOT NULL替代NOT IN,NULL语义会出错 - 无论
EXISTS还是JOIN,都要求关联字段(如t2.id)有索引,否则照样全表扫
临时表+批量INSERT比拼接大IN字符串更稳
当业务真没法避免“传几千个ID进来”(比如同步任务、导出筛选),硬拼IN (1,2,3,...)不如走临时表。关键是把数据写入过程和查询分离,避免单条SQL过大,同时给临时表建主键或唯一索引,让JOIN走ref而非ALL。
- 用
CREATE TEMPORARY TABLE temp_ids (id BIGINT PRIMARY KEY),别漏PRIMARY KEY -
INSERT时用VALUES (1),(2),(3),...,(1000)分批,每批≤1000行,避免单次INSERT超max_allowed_packet - 最后
SELECT * FROM main_table JOIN temp_ids ON main_table.id = temp_ids.id,执行计划里type应为ref
真正卡住人的从来不是“要不要换写法”,而是没意识到IN在不同规模、不同上下文(常量 vs 子查询)、不同MySQL版本里的行为差异极大。哪怕加了索引,一个NOT IN带NULL就能让所有优化归零。











