MySQL 5.7 MRR多范围读优化

MRR (Multi-Range Read,多范围读) 是MySQL 5.7 默认启用的一个优化策略。

原理

Server 层执行器要数据,得通过存储引擎 API。在没有 MRR 的时候,执行器拿回表数据的方式是一次要一条:

没有 MRR 的调用过程:

Server: 引擎,把主键=12 的行给我
InnoDB: (去聚集索引读一次 → 返回)
Server: 引擎,把主键=58 的行给我
InnoDB: (再读一次 → 返回)
Server: 引擎,把主键=3291 的行给我
InnoDB: (再读一次 → 返回)
...重复 500 次...

每次调用引擎只能看到"一条主键",
它没法提前知道下一个要读什么 → 无法规划 → 只能来一条读一条(乱序随机)

有了 MRR,API 变成了一次给一批:

有 MRR 的调用过程:

Server: 引擎,主键在 [12, 58, 3291, 8812, ...] 这个"多范围"集合里的行,一次都给我
InnoDB: (看到完整批次了!)
        → 我先排序 → 12, 58, 3291, 8812
        → 我再按页合并 → P1(12,58), P6(3291), P34(8812)
        → 按页顺序批量读 → 一起返回

即:

没有 MRR:
    通过二级索引找到多个主键值 → 按主键值出现的顺序随机回表 I/O

有 MRR:
    通过二级索引找到多个主键值 → 先排序主键 → 按主键顺序批量回表
    → 磁盘随机 I/O 变顺序 I/O → 更快

收益好处

MRR 带来的收益是真实的,收益来自三个方面:

  1. 单调递增的页访问 ≠ 乱序跳转

    聚集索引的叶子页在磁盘上按主键顺序排列(B+Tree 的本质):

      页P1       页P2       页P3    ...    页P20
    [id=1~100][id=101~200][id=201~300]  [id=1901~2000]
         ↑
      PK=12, 580, 3291, 8812 分别落在 P1, P6, P34, P89
    

    没有 MRR(按二级索引扫出来的顺序回表):
    扫二级索引得到:8812, 12, 3291, 580, ...
    回表顺序:P89 → P1 → P34 → P6 → ...
    磁盘磁头:跳到89 → 跳回1 → 跳到34 → 跳回6
    HDD 每次跨页寻道 5-10ms,来回横跳

    有 MRR(先排序再回表):
    排序后:12, 580, 3291, 8812
    回表顺序:P1 → P6 → P34 → P89
    磁头朝一个方向走,每次只跨几个磁道
    → 不是严格连续,但接近顺序扫的代价

    HDD 上"单调前进"和"来回横跳"的差距就是好几倍,这跟电梯类比:从 1 楼 → 6 楼 → 34 楼 → 89 楼往上走,比 89 → 1 → 34 → 6 来回折返快得多。

  2. 同一页内的多条主键,合并成一次读取

    这是 MRR 收益最大的一块:

    二级索引扫出 500 个主键,其中 30 个落在 P1(id=1~100)
    没有 MRR:
    回表读 id=12 时读一次 P1
    过一会回表读 id=58 时,P1 可能已被挤出 Buffer Pool → 再读一次 P1
    最坏情况:同一个页被读了 30 次

    有 MRR:
    排序后,P1 里的 30 个主键集中处理
    读一次 P1,把 30 行全取出来
    → 500 次回表可能压缩成 80 次页读取

  3. 预读(read-ahead)友好

    磁盘/OS 层都有预读机制:你读了 P6,它顺手把 P7、P8 也捞进缓存。乱序访问时预读的页用不上就浪费了;单调递增访问时预读的页大概率马上用到。


总结

▎ MRR 把"乱序回表"变成"按主键单调递增的页访问",虽然不是物理连续的顺序 I/O,但实现了:
▎ ① 磁头单向移动(减少寻道) ② 同页主键合并读取(减少重复读页) ③ 预读友好。

▎ 本质是降低随机性,而不是达到严格顺序。

另外 :MySQL 5.7 里 MRR 默认开着,但 mrr_cost_based=on 意味着优化器会按成本决定用不用;在 HDD 上收益明显,在 SSD 上收益小但仍有效(因为减少了 I/O 请求次数)。

posted on 2026-08-13 14:21  幽州散人  阅读(13)  评论(0)    收藏  举报

导航