云计算百科
云计算领域专业知识百科平台

[小技巧27]MRR 如何重塑回表 I/O:从随机访问到顺序读取的性能跃迁

一、什么是 MRR(Multi-Range Read)?

MRR(Multi-Range Read) 是 MySQL 从 5.6 版本开始引入的一种优化技术,主要用于优化 基于非聚簇索引(Secondary Index)进行范围查询或 IN 查询后回表(lookup)主键数据 的 I/O 效率。

在没有 MRR 的情况下,MySQL 执行器会逐行通过二级索引查到主键值后,立即去聚簇索引(InnoDB 的主键索引)中查找对应的完整行数据。这种“随机 I/O”模式在大量回表操作时效率极低。

MRR 的核心思想是:将多个需要回表的主键值先收集起来,排序后再批量顺序读取聚簇索引中的数据页,从而将原本的随机 I/O 转化为近似顺序 I/O,大幅提升磁盘读取效率(尤其对机械硬盘效果显著)。

二、MRR 的工作原理

1. 基本流程(以 InnoDB 引擎为例):

  • 使用二级索引扫描:执行器根据 WHERE 条件(如 WHERE key_col BETWEEN 10 AND 100 或 WHERE key_col IN (1, 5, 100…))在二级索引上找到匹配的记录。
  • 暂存主键值:不立即回表,而是将这些记录对应的主键值(或行指针)放入一个缓冲区(称为 read_rnd_buffer)。
  • 排序主键值:当缓冲区满或扫描结束时,对其中的主键值进行排序(按主键顺序)。
  • 批量顺序回表:按排序后的主键顺序,批量访问聚簇索引,读取完整的行数据。
  • 返回结果:最后按原始查询顺序(如有 ORDER BY)重新排序并返回。
  • MRR 在 Server 层中“发挥作用”,但需要存储引擎配合才能生效

    2. 关键参数

    • read_rnd_buffer_size:控制 MRR 缓冲区大小(单位字节),默认 256KB。增大该值可提升 MRR 批量处理能力,但会占用更多内存。
    • optimizer_switch 中的 mrr 和 mrr_cost_based:
      • mrr=on/off:是否启用 MRR。
      • mrr_cost_based=on/off:是否基于成本模型决定是否使用 MRR(默认 on)。若设为 off,则只要可能就强制使用 MRR。

    可通过以下命令查看:

    SELECT @@optimizer_switch;
    — 示例输出中包含:mrr=on,mrr_cost_based=on

    三、MRR 的适用场景

    MRR 主要在以下场景下生效:

  • 二级索引范围查询 + 回表

    SELECT * FROM t WHERE idx_col BETWEEN 100 AND 200;

  • IN 查询 + 回表

    SELECT * FROM t WHERE idx_col IN (1, 5, 10, 100);

  • JOIN 中使用二级索引进行 lookup(Batched Key Access, BKA)
    BKA 是 MRR 的扩展,用于优化 JOIN。当使用 JOIN 且驱动表结果用于被驱动表的索引查找时,BKA 会利用 MRR 批量获取被驱动表的数据。

  • ⚠️ 注意:MRR 仅适用于 需要回表 的情况。如果查询是覆盖索引(Covering Index),即所有字段都在二级索引中,则无需回表,MRR 不会触发。

    四、MRR 的优势与局限

    优势:

    • 显著减少随机 I/O:将多次随机磁盘访问合并为顺序访问,对 HDD 性能提升明显(SSD 上也有一定收益)。
    • 提高缓存命中率:顺序读取更利于 InnoDB Buffer Pool 的局部性原理。
    • 与 BKA 联动优化 JOIN:在多表连接中大幅减少嵌套循环的 I/O 开销。

    局限:

    • 增加 CPU 和内存开销:需要排序主键值,消耗 CPU;缓冲区占用内存。
    • 可能打乱结果顺序:若查询依赖原始顺序(如无 ORDER BY 但应用假定顺序),需注意 MRR 可能改变返回顺序(不过 MySQL 通常会保证逻辑正确性)。
    • 成本模型可能误判:在 mrr_cost_based=on 时,优化器可能因统计信息不准而错误地禁用 MRR。
    • 对 SSD 效果减弱:SSD(Solid State Drive,固态硬盘) 随机读性能接近顺序读,MRR 收益不如 HDD(Hard Disk Drive,机械硬盘) 明显。

    五、MRR 与相关技术对比

    技术目的是否需要回表I/O 模式优化
    MRR 优化二级索引回表 随机 → 顺序
    覆盖索引(Covering Index) 避免回表 完全避免回表 I/O
    ICP(Index Condition Pushdown) 减少回表次数 在存储引擎层过滤,减少不必要的回表
    BKA(Batched Key Access) 优化 JOIN 中的 lookup 基于 MRR 实现批量回表

    实际中,MRR 常与 ICP、BKA 配合使用,形成组合优化。

    六、MRR 如何与 Buffer Pool 交互?

    1. 提升 Buffer Pool 命中率(Cache Locality)

    在非 MRR 模式下:

    • 回表操作按二级索引的顺序进行,而二级索引的主键值通常是无序的(除非是自增主键且插入有序)。
    • 导致聚簇索引页的访问呈随机分布,可能频繁访问不同数据页,超出 Buffer Pool 容量后造成大量换入换出(thrashing)。

    启用 MRR 后:

    • 主键值被排序,回表时按主键升序访问聚簇索引。
    • 聚簇索引在物理上按主键顺序存储,因此连续的主键往往落在相邻的数据页中。
    • 这使得:
      • 已加载到 Buffer Pool 的页更可能被后续请求复用(空间局部性增强);
      • 即使页不在内存中,顺序读也更容易触发 InnoDB 的线性预读(linear read-ahead),提前加载后续页。

    结果:Buffer Pool 命中率提高,物理 I/O 次数进一步减少。

    2. 激活 InnoDB 预读机制(Read-Ahead)

    InnoDB 有两种预读机制:

    • 随机预读(Random Read-Ahead):当某个区(extent, 1MB)中有一定数量的页被访问,就预读整个区(MySQL 5.6+ 默认关闭)。
    • 线性预读(Linear Read-Ahead):当顺序访问某个区中的多个页(由 innodb_read_ahead_threshold 控制,默认 56 页),则预读下一个区。

    MRR 排序后的主键访问模式天然符合线性预读的触发条件。例如:

    主键排序后:1001, 1002, …, 1100
    → 对应聚簇索引页可能是 page#100, page#101, page#102…
    → 顺序访问 → 触发 linear read-ahead → 提前加载 page#103~112

    这在扫描大量数据时尤为有效,大幅减少 I/O 等待。

    注意:若 read_rnd_buffer_size 设置过小,MRR 批次太小,可能不足以触发预读。

    3. 减少 Buffer Pool 污染(Pollution)

    在高并发 OLTP 场景中,大量随机回表可能导致:

    • 大量“一次性”数据页被加载进 Buffer Pool;
    • 挤占热点数据页,导致缓存效率下降(即“缓存污染”)。

    MRR 通过集中、批量、顺序访问,使得:

    • 数据页访问更集中;
    • 更容易形成“热区”,而非分散的冷页;
    • 有利于 InnoDB 的 old sublist 机制(将新读入的页先放入 old 区,避免立即淘汰热数据)。

    七、总结(Summary)

    MRR(Multi-Range Read)是 MySQL 针对 二级索引回表操作 的一项重要 I/O 优化技术。
    其核心在于 将离散的主键查找请求缓冲、排序后批量顺序执行,从而将高成本的随机 I/O 转化为高效的顺序 I/O。
    它在机械硬盘环境下效果尤为显著,同时为 BKA(批量键访问)提供基础支持,极大提升了复杂查询(尤其是多表 JOIN)的性能。

    尽管 MRR 会带来一定的 CPU 和内存开销,且在 SSD 环境下收益减弱,但在大多数 OLTP 和混合负载场景中,合理启用 MRR 仍是提升查询效率的有效手段。

    八、面试可能问到的问题

    问题 1:

    MySQL 中的 MRR(Multi-Range Read)是什么?它在什么场景下生效?为什么能提升性能?

    考察点:对 MRR 原理、适用条件和性能收益的理解。

    问题一:

    MRR主要用于优化通过二级索引进行范围查询或 IN 查询后回表(lookup)主键数据的过程。

    问题二: 生效场景

    MRR 在以下条件同时满足时才会被优化器考虑使用:

    • 查询使用了非聚簇索引(Secondary Index);
    • 该索引不能覆盖查询所需的所有字段(即需要回表);
    • 查询条件涉及多个离散值或范围,例如 WHERE idx_col IN (1, 5, 100) 或 BETWEEN;
    • 优化器认为启用 MRR 能降低执行成本(可通过 optimizer_switch='mrr=on,mrr_cost_based=off' 强制开启)。

    问题3. 工作原理与性能提升原因

    在没有 MRR 时,MySQL 会逐行通过二级索引拿到主键后立即回表,导致大量随机 I/O。

    而 MRR 的做法是:

    • 先将所有需要回表的主键值收集到一个缓冲区(read_rnd_buffer);
    • 对这些主键排序;
    • 然后按主键顺序批量访问聚簇索引

    这样做的好处是:

    • 将原本的随机 I/O 转化为近似顺序 I/O,大幅减少磁盘寻道时间(尤其对 HDD 效果显著);
    • 提升 InnoDB Buffer Pool 的空间局部性,提高缓存命中率;
    • 更容易触发 InnoDB 的线性预读(linear read-ahead),进一步减少物理 I/O。

    问题 2:

    MRR 和覆盖索引(Covering Index)都能优化查询性能,它们有什么区别?在什么情况下应该优先考虑哪一种?

    考察点:对不同优化技术的对比分析能力,以及实际调优中的权衡思维。

    问题1. 核心区别

    维度MRR(Multi-Range Read)覆盖索引(Covering Index)
    目标 优化必须回表时的 I/O 效率 避免回表,完全从索引中获取数据
    是否需要访问聚簇索引 是(但以更高效方式)
    I/O 类型 将随机 I/O 转为顺序 I/O 完全消除回表 I/O
    存储开销 无额外索引开销 需要创建包含更多列的宽索引,占用更多磁盘和内存
    适用查询 SELECT * 或含非索引列的查询 SELECT 列全部包含在索引中的查询

    问题2. 优先级原则:

    覆盖索引 > MRR

    • 首选覆盖索引:

      如果业务查询的字段集合固定且不大,应优先设计覆盖索引。因为不回表是最彻底的优化,无论底层是 HDD 还是 SSD 都有显著收益,且 CPU 开销更低。

      示例:

      — 若经常查 (user_id, status),可建联合索引
      CREATE INDEX idx_user_status ON orders(user_id, status);
      SELECT user_id, status FROM orders WHERE user_id = 123; — 覆盖索引,无需回表

    • 次选 MRR:

      当无法使用覆盖索引时(如 SELECT *、查询字段太多、或字段含大文本),MRR 成为重要的兜底优化手段,尤其在 HDD 环境或大批量回表场景下。

    问题3. 实际调优建议

    • 覆盖索引虽好,但不是万能的:索引过宽会拖慢写入性能、增大 Buffer Pool 压力;
    • MRR 是执行层的运行时优化,无需改表结构,但依赖查询模式和硬件特性;
    • 最佳实践:先尝试覆盖索引,若不可行再确保 MRR 可用并合理配置 read_rnd_buffer_size。

    总结一句话
    覆盖索引是从“要不要回表”上做根本性优化,MRR 是在“不得不回表”的前提下做效率优化——前者优先级更高。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » [小技巧27]MRR 如何重塑回表 I/O:从随机访问到顺序读取的性能跃迁
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!