IN子句参数超200个会触发硬解析失败、索引失效及执行计划退化,主因是eq_range_index_dive_limit限制、max_allowed_packet截断及优化器弃用索引;应改用带主键的临时表JOIN,并确保类型、字符集、排序规则一致。

IN子句塞几千个值,不是“有点慢”,而是大概率触发硬解析失败、索引失效、执行计划退化,甚至直接报错——这不是SQL写得不够好,是MySQL内部机制决定的。
eq_range_index_dive_limit 默认200,超了就放弃精算索引成本
MySQL优化器对IN列表做成本估算时,会扫描索引树(index dive)来统计匹配行数。但这个动作只在IN值数量 ≤ eq_range_index_dive_limit(默认200)时才执行;超过后,改用粗略的统计值估算,极易选错执行计划。
- 常见现象:
EXPLAIN显示type: ALL或key: NULL,明明字段有索引却走全表扫描 - 影响范围:哪怕只有201个值,优化器也可能从
range降级为index或ALL - 不能靠调大该参数治本:设成1000只是推迟问题,语法树构建和内存开销仍在恶化
max_allowed_packet 和 net_buffer_length 直接拦在解析前
拼出来的SQL字符串长度一旦超过 max_allowed_packet(默认4MB)或客户端缓冲区 net_buffer_length(默认16KB),查询连进优化器的机会都没有。
- 典型错误:
Packet for query is too large—— 连EXPLAIN都跑不了 - 客户端驱动更脆弱:MySQL Connector/J 在预编译阶段可能静默截断长参数列表,查不到数据却没报错
- 别只看“值个数”:UUID字符串比INT占3倍以上字节,1000个UUID很可能就超限
临时表 + JOIN 不是加个表就行,主键和引擎选错等于白干
把IN改成JOIN,关键不在语法替换,而在让临时数据可驱动、可索引、可控制。
-
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY)必须带PRIMARY KEY,否则没索引,JOIN退化为嵌套循环 - 引擎别乱选:
ENGINE=MEMORY受max_heap_table_size限制(默认16MB),插10万个BIGINT就爆内存;含字符串或超5万行,直接切ENGINE=InnoDB - 批量插入必须用
INSERT INTO tmp_ids VALUES (1),(2),(3),...,(1000),单条INSERT 1000次比一次插1000行慢10倍以上 - INSERT完立刻
ANALYZE TABLE tmp_ids(尤其InnoDB),否则优化器不知道新表有多少行
分批查询容易漏掉三个隐性边界
拆成多个WHERE id IN (...)看似简单,但实际落地常因细节失控导致结果错、性能反降。
- 批次大小不能死守1000:按实际ID平均字节动态算,目标是单条SQL总长 max_allowed_packet * 0.7
- 结果合并必须去重:数据库不保证各批次返回顺序一致,应用层用
Set合并时,若原始ID有重复,会丢数据 - 事务一致性难保:多批次执行期间,目标表数据可能被并发修改,导致漏查或重复查
真正卡住的从来不是“能不能写出来”,而是临时表有没有主键、JOIN驱动顺序对不对、分批时字节数算没算准——这些点不显眼,但一漏就全盘失效。



















