回表慢的核心是随机IO:二级索引查出的主键乱序,导致聚簇索引查找频繁跨页加载;MRR通过缓冲+排序主键,将随机IO转为顺序IO,但需显式开启、足够缓冲区且仅对range/ref生效。

回表为什么慢:随机IO是核心瓶颈
二级索引查出主键后,再拿这些主键去聚簇索引里捞数据,这个“再捞一次”的过程就是回表。问题在于:二级索引叶子节点里的主键值通常是乱序的(比如先查到 id=24,接着是 id=82546),导致每次回表都要跳到磁盘上完全不相邻的页——这就是随机IO。InnoDB 以页为单位加载数据,一次只读一页(16KB),但若每条记录都触发一次新页加载,Handler_read_rnd 会飙升,缓冲池命中率断崖式下跌。
MRR如何把随机IO变成顺序IO
MRR(Multi-Range Read)不是改索引结构,而是加了一层缓冲调度:优化器先把所有要回表的主键暂存进 read_rnd_buffer_size 缓冲区,排序后再批量按主键顺序发起聚簇索引查找。这样原本散落在几百个页的回表请求,可能被合并成对几十个页的顺序读取。
-
read_rnd_buffer_size默认通常只有 256KB,大查询时容易溢出退化为普通回表;建议根据单行主键+数据页大小预估,设为 1MB~4MB 更稳妥 - 必须显式开启:
SET optimizer_switch='mrr=on,mrr_cost_based=off';,否则优化器可能因成本估算不准而跳过MRR - MRR只对
range和ref访问类型生效,eq_ref或全表扫描不触发 - 在
EXPLAIN的Extra列看到Using MRR才算真正启用
覆盖索引和MRR不是互斥关系,而是分层策略
覆盖索引(Extra: Using index)是根治回表的手段,MRR 是“不得不回表时”的兜底优化。但二者有现实冲突点:
- 联合索引字段越多越容易覆盖,但索引体积膨胀 → 内存中能缓存的页数减少 → 可能反而拖慢范围扫描本身
- MRR 对宽索引(比如
(a,b,c,id))效果更明显,因为一次能攒更多主键;但若索引已覆盖全部SELECT字段,MRR 根本不会启动 - ORDER BY 涉及非索引字段时,即使用了覆盖索引,也可能因
Using filesort导致临时表 + 回表连环触发,此时MRR仍可能介入
验证MRR是否生效的关键检查点
别只看 EXPLAIN,真实负载下MRR可能被静默禁用:
- 执行
SHOW VARIABLES LIKE 'read_rnd_buffer_size';确认值足够,且未被会话级重置 - 查状态变量:
SHOW STATUS LIKE 'Handler_read_rnd%';,如果Handler_read_rnd_next远高于Handler_read_rnd,说明MRR没起作用(后者才代表MRR启用后的批量读) - 用
SELECT /*+ USE_INDEX_MRR(t, idx) */ * FROM t WHERE ...强制走MRR路径做对比测试 - 注意:5.7+ 默认开启MRR,但云厂商RDS常默认关闭或限制缓冲区大小,得手动确认
实际调优时,优先保证关键查询走覆盖索引;当字段太多无法覆盖、或业务无法改SQL时,MRR才是那个真正扛住高并发回表压力的缓冲阀——但它依赖缓冲区大小、主键分布、以及优化器开关三者同时在线。











