线上商城后台有个订单查询接口,运营人员从报表里复制出一批订单号,塞到IN条件里,一次查几千上万个订单的明细。上线后某个大促夜,这个接口直接把数据库的连接池拖满了,慢SQL日志里全是那条带几千个ID的IN查询,单次执行时间八九秒。当时第一反应是骂运营不会用系统,但冷静下来一想,这需求还真回避不了——业务侧就是需要按一批已知ID去批量捞数据。IN查询大数据量,在不少业务里是刚需,不是写SQL的人水平差。
这个题网上讨论不少,但多数是零散的“加个索引”“改JOIN”,真到自己动手时,你会发现每一条都有前提条件,没有一条是银弹。这篇文章我把自己这几年在MySQL里跟大IN查询死磕的过程——原理层面的、SQL改写层面的、参数层面的、架构兜底层面的——系统梳理一遍。内容主要面向后端开发、DBA和线上系统出过类似问题的运维同学,目标是看完之后你能根据自己业务的实际情况选对方案,而不是拿着别人的结论盲目抄。
1. 一个真实事故:几千个ID的IN查询拖垮接口
先说当时的现场。系统是典型的电商后台订单中心,MySQL 8.0,订单主表order_info有三千多万行,索引齐全,日常查询都在毫秒级。运营用的批量查询功能,逻辑很简单:接收一批订单号,拼成 IN 条件,去order_info表查出完整订单信息,返回给前端展示。本地测试时数据量小、压测也没问题,但大促期间运营手里的订单号列表动辄几千上万,这条SQL直接成了定时炸弹。
慢日志里抓到的SQL长这样:
sql复制SELECT *
FROM order_info
WHERE order_no IN
('A10001','A10002', ... /* 一共 9400 多个订单号 */)
ORDER BY create_time DESC;
执行计划打印出来,type是ALL,全表扫描,rows估算30万行,Extra里还带着Using filesort。这不是索引失效,是优化器在IN列表太长时压根没选择走索引。当时第一件事是把IN列表截短到500个做测试——执行时间从8.4秒降到了0.3秒。问题实锤了,但运营不可能手动拆分9000多个订单号去查,产品也不能说“查询上限500条”就算完事。
后来排查发现,这个慢还不只是全表扫描的问题。即使把SQL强改成FORCE INDEX(uk_order_no)让它走索引,单次查询也要2秒以上。原因很容易理解:9000多个订单号,每个订单号在辅助索引上命中的索引项,都要拿着主键ID去聚簇索引回表捞整行数据。回表是随机IO,9000次随机IO在机械盘上那就是灾难,SSD上虽然好一些,但高并发下一样扛不住。
那次的最终方案是分片查询——9000多个订单号切分成每批500个,串行查询,总耗时从8秒多降到2秒左右,再加一层Redis结果缓存,运营的体感提升非常明显。但这只是治标,治本还得从MySQL的执行机制去理解,为什么IN列表一大,性能就断崖式下跌。下面这节把底层原因拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么IN列表一大,性能就断崖下跌:从执行计划到回表成本
网上有句话流传很广:“IN查询不会导致索引失效。”这话对,但没有说到点子上。IN查询的问题从来不是“不能走索引”,而是“走了索引也很慢”,以及“优化器在特定条件下会放弃索引”。
2.1 优化器对IN列表的估算机制:eq_range_index_dive_limit是关键门槛
MySQL优化器对IN列表的处理,本质上是生成多个等值条件的OR展开,然后估算每个分支的基数,最后合并成一个执行计划。这里有个关键参数叫 eq_range_index_dive_limit,默认值是200。
它的含义是:当IN列表中的值个数不超过200时,优化器会对每个值真正去索引B+树里做一次“潜入式统计”(index dive),拿到精确的rows估算值;一旦超过200,优化器就不再逐个潜入,而是直接拿索引的统计信息去估算。索引统计信息是抽样出来的,碰到数据分布不均匀时,估算偏差会非常大。
比如某个订单号在索引里实际有几十条记录,但统计信息显示它只有1条,优化器就会觉得“走这个索引成本很低”,结果真实执行时发现每个值都要捞出大量行,还要挨个回表。反过来也可能把成本估高了,直接放弃索引走上全表扫描。后者就是我们线上看到的现象。
所以,当你看到一条大IN查询的执行计划变成ALL时,第一步不是急着加索引,而是先看一眼IN列表数量是不是超过了200。超过的话,调大eq_range_index_dive_limit只是一个可选项,但它会让每条SQL的解析阶段变慢,全局影响面很大,不能无脑调。
2.2 索引命中了,回表依然是大头成本
假设优化器已经正确选择了走索引,比如order_no上的唯一索引uk_order_no,那问题就全落在回表上。
做一次索引查询的路径是这样的:先在辅助索引B+树里定位到所有满足条件的叶子节点,拿到一批主键ID,然后再拿着这批ID去聚簇索引里查完整行数据。辅助索引本身的扫描是相对顺序的,但回表是按主键ID去聚簇索引里随机查找。主键ID在物理存储上并不连续,每回一次表都可能触发一次磁盘随机IO。
一次回表耗时大约在0.1ms到
