MySQL 优化器 MRR

什么是 MRR

MRR 的全称是 Multi-Range Read Optimization,是优化器将随机 IO 转化为顺序 IO 以降低查询过程中 IO 开销的一种手段,咱们对比一下 mrr=on & mrr=off 时的执行计划:

其中表结构如下:

sql 复制代码
mysql> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `mrrx` (`a`,`b`),
  KEY `xx` (`c`)
) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

操作如下:

sql 复制代码
mysql> set optimizer_switch='mrr=off';
Query OK, 0 rows affected (0.00 sec)

mysql>  explain select * from test.t1 where (a between 1 and 10) and (c between 9 and 10) ;
+----+-------------+-------+-------+---------------+------+---------+------+------+------------------------------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                              |
+----+-------------+-------+-------+---------------+------+---------+------+------+------------------------------------+
|  1 | SIMPLE      | t1    | range | mrrx,xx       | xx   | 5       | NULL |    2 | Using index condition; Using where |
+----+-------------+-------+-------+---------------+------+---------+------+------+------------------------------------+
1 row in set (0.00 sec)

当把 MRR 关掉的情况下,执行计划使用的是索引 xx(c),即从索引 xx 上读取一条数据后回表,取回该主键的完整数据,当数据较多且比较分散的情况下会有比较多的随机 IO, 导致性能低下,我们将 MRR 打开,执行以下操作:

sql 复制代码
mysql> set optimizer_switch='mrr=on';
Query OK, 0 rows affected (0.00 sec)

mysql>  explain select * from test.t1 where (a between 1 and 10) and (c between 9 and 10) ;
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------------------------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                                         |
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------------------------------+
|  1 | SIMPLE      | t1    | range | mrrx,xx       | xx   | 5       | NULL |    2 | Using index condition; Using where; Using MRR |
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------------------------------+
1 row in set (0.00 sec)

可以看到 extra 的输出中多了 "Using MRR" 信息,即使用了 MRR Optimization IO 层面进行了优化,减少 IO 方面的开销,更详细的说明可以参考这里

MRR 原理

在不使用 MRR 时,优化器需要根据二级索引返回的记录来进行"回表",这个过程一般会有较多的随机 IO, 使用 MRR 时,SQL 语句的执行过程是这样的:

  • 优化器将二级索引查询到的记录放到一块缓冲区中;
  • 如果二级索引扫描到文件的末尾或者缓冲区已满,则使用快速排序对缓冲区中的内容按照主键进行排序;
  • 用户线程调用 MRR 接口取 cluster index,然后根据cluster index 取行数据;
  • 当根据缓冲区中的 cluster index 取完数据,则继续调用过程 2) 3),直至扫描结束;

通过上述过程,优化器将二级索引随机的 IO 进行排序,转化为主键的有序排列,从而实现了随机 IO 到顺序 IO 的转化,提升性能。

MRR 源码分析

首先,咱们来看一下 mrr 相对应的内存结构:

sql 复制代码
class DsMrr_impl
{
  ...
  handler *h;
  TABLE *table; /* Always equal to h->table */
private:
  /* Secondary handler object.  It is used for scanning the index */
  handler *h2;

  /* Buffer to store rowids, or (rowid, range_id) pairs */
  uchar *rowids_buf;
  uchar *rowids_buf_cur;   /* Current position when reading/writing */
  uchar *rowids_buf_last;  /* When reading: end of used buffer space */
  uchar *rowids_buf_end;   /* End of the buffer */

  bool dsmrr_eof; /* TRUE <=> We have reached EOF when reading index tuples */

  int dsmrr_init(handler *h, RANGE_SEQ_IF *seq_funcs, void *seq_init_param,
                 uint n_ranges, uint mode, HANDLER_BUFFER *buf);
  ....
  int dsmrr_fill_buffer();
  int dsmrr_next(char **range_info);
  bool get_disk_sweep_mrr_cost(uint keynr, ha_rows rows, uint flags, uint *buffer_size, Cost_estimate *cost);
  ....
}

简单说明:h2 指的是 MRR 使用的 second index 或主键索引, h 是指利用 h2 返回的主建来查询的句柄,rowids_buf 是 MRR 执行过程中存储有序主键的缓存区,大小由 MySQL 的变量 read_rnd_buffer_size 设置,下面我们结合程序的执行过程来看一下源码。

1)MRR 中有序主建的收集过程

优化器对查询语句的条件进行分析并选择合适的二级索引,并对二级索引的条件进行筛选拼装成 DYNAMIC_ARRAY ranges,在执行的时候将 ranges 传入初始化函数 ha_myisam::multi_range_read_init ,继而会调用 dsmrr_fill_buffer 函数,在dsmrr_fill_buffer中会使用二级索引的句柄查找符合 ranges 的数据并添加至 rowids_buf 中,在扫描结束或缓冲区满的时候会对 rowids_buf 进行快速排序,详细过程可以参考函数:dsmrr_fill_buffer,其调用堆栈下:

sql 复制代码
 #0  DsMrr_impl::dsmrr_fill_buffer (this=0x2aab0000cf00)
 #1  0x00000000006e49dd in DsMrr_impl::dsmrr_init(...)
 #2  0x00000000017d35e4 in ha_myisam::multi_range_read_init(...)
 #3  0x0000000000d134c6 in QUICK_RANGE_SELECT::reset (this=0x2aab00014070)
 #4  0x00000000009a266f in join_init_read_record (tab=0x2aab0000f5b8)
 #5  0x000000000099d6d4 in sub_select
 #6  0x000000000099c914 in do_select (join=0x2aab000064b0)
 #7  0x00000000009982f8 in JOIN::exec (this=0x2aab000064b0)
 #8  0x0000000000a5bd7c in mysql_execute_select
 ........

2)MRR 中主建缓冲区的使用过程

物理执行阶段,调用 ha_myisam::multi_range_read_next,在使用 MRR 的情况下会从过程1)中收集的有序主键的缓冲区取主键,然后再调用引擎层的 rnd_pos 直接找到数据,其中使用 mrr 的调用堆栈如下:

sql 复制代码
 #0  DsMrr_impl::dsmrr_next (this=0x2aab0000cf00, range_info=0x2aaafc03de70)
 #1  0x00000000017d3634 in ha_myisam::multi_range_read_next (this=0x2aab0000ca40, range_info=0x2aaafc03de70)
 #2  0x0000000000d138cc in QUICK_RANGE_SELECT::get_next (this=0x2aab00014070)
 #3  0x0000000000d46908 in rr_quick (info=0x2aab0000f648)
 #4  0x00000000009a2791 in join_init_read_record (tab=0x2aab0000f5b8)
 #5  0x000000000099d6d4 in sub_select (join=0x2aab000064b0, join_tab=0x2aab0000f5b8, end_of_records=false)
 #6  0x000000000099c914 in do_select (join=0x2aab000064b0)

二缓索引(h2)& 主建索引(h) 的协同是通过rowids_buf_cur来进行的。最初的初始化过程中,h2 会首先将数据填冲到 rowids_buf 中,如果发现缓冲区中的数据已经取完,则会继续调用 dsmrr_fill_buffer 往 rowids_buf 填主键并进行排序,如此反复,直至 h2 扫描至文件末尾,详情可以参考函数 DsMrr_impl::dsmrr_next。

通过上面的分析,是不是感觉 MRR 有点像二级索引与主键的 join 操作,那就是有点和 BKA 有些类似的概念了,咱们下面看一下 BKA 是如何实现的。

MRR 使用场景

场景A:对于InnoDB和MyISAM表的索引范围扫描和等值连接操作,可以使用MRR优化。

执行流程:

  • 索引的一部分元组被累积在缓冲区中。
  • 缓冲区中的元组按其数据行 ID 进行排序。
  • 根据排序后的索引元组序列访问数据行。

场景举例:

1)索引范围扫描:假设有一个名为orders的表,其中包含order_id和order_date列,并且为order_date列创建了一个索引。当执行以下查询时:

sql 复制代码
SELECT order_id FROM orders WHERE order_date BETWEEN '2022-01-01' AND '2022-12-31';

MRR优化可以使用索引的范围扫描,将满足条件的索引元组收集到缓冲区中,并按照数据行ID进行排序。然后,根据排序后的索引元组顺序访问数据行,而无需回表操作。

2)等值连接:假设有两个表orders和customers,它们之间通过customer_id列进行连接。当执行以下查询时:

sql 复制代码
SELECT order_id FROM orders INNER JOIN customers ON orders.customer_id = customers.customer_id;

MRR优化可以使用等值连接操作,将满足条件的索引元组收集到缓冲区中,并按照数据行ID进行排序。然后,根据排序后的索引元组顺序访问数据行,以执行等值连接操作。

场景B:对于NDB表的多范围索引扫描或按属性进行等值连接时,可以使用MRR优化。

执行流程:

  • 一部分范围(可能是单键范围)在提交查询的中央节点上被累积在缓冲区中。
  • 范围被发送到访问数据行的执行节点。
  • 访问的行被打包成数据包并发送回中央节点。
  • 接收到的带有数据行的数据包被放置在缓冲区中。
  • 数据行从缓冲区中读取。

场景举例:

1)多范围索引扫描:假设有一个NDB表products,其中包含product_id和price列,并且为price列创建了一个多范围索引。当执行以下查询时:

sql 复制代码
SELECT product_id FROM products WHERE price BETWEEN 10 AND 100;

MRR优化可以在查询提交的中央节点上,将满足条件的一部分范围(可能是单键范围)累积到缓冲区中。然后,将这些范围发送到访问数据行的执行节点。执行节点将访问的行打包并发送回中央节点。中央节点接收到包含数据行的包后,将其放入缓冲区。然后,可以从缓冲区中读取数据行。

2)按属性进行等值连接:假设有两个NDB表

orders和customers,它们之间通过customer_id列进行连接。当执行以下查询时:

sql 复制代码
SELECT order_id FROM orders INNER JOIN customers ON orders.customer_id = customers.customer_id;

MRR优化可以使用等值连接操作,将满足条件的一部分范围(可能是单键范围)累积到中央节点的缓冲区中。然后,这些范围被发送到访问数据行的执行节点。执行节点将访问的行打包并发送回中央节点。中央节点接收到包含数据行的包后,将其放入缓冲区。然后,可以从缓冲区中读取数据行。

MRR 如何使用

sql 复制代码
//如果你不打开,是一定不会用到 MRR 的
set optimizer_switch='mrr=on';
set optimizer_switch ='mrr_cost_based=off';
set read_rnd_buffer_size = 32 * 1024 * 1024;

mrr_cost_based:on/off,是用来告诉优化器,要不要基于使用 MRR 的成本,考虑使用 MRR 是否值得(cost-based choice),来决定具体的 sql 语句里要不要使用 MRR。

很明显,对于只返回一行数据的查询,是没有必要 MRR 的,而如果你把 mrr_cost_based 设为 off,那优化器就会通通使用 MRR,这在有些情况下是很 stupid 的,所以建议这个配置还是设为 on,毕竟优化器在绝大多数情况下都是正确的。

相关推荐
开心呆哥30 分钟前
【如何使用 Python 脚本通过 ADB 命令来检查 Android 文件内容】
android·python·adb
秋秋秋秋秋雨21 小时前
linux强制修改mysql的root账号密码
linux·mysql·adb
开心呆哥1 天前
【如何使用 ADB 脚本批量停止 Android 设备上的所有应用】
android·adb
CodingBrother2 天前
MySQL 和 PostgreSQL 的使用案例
mysql·adb·postgresql
白乐天_n2 天前
adb:Android调试桥
android·adb
风和先行2 天前
adb 命令查看设备存储占用情况
android·adb
小兜全糖(xdqt)2 天前
mysql数据同步到sql server
mysql·adb
帅得不敢出门3 天前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
鬼才血脉3 天前
docker+mysql配置
mysql·adb·docker
CircleMouse3 天前
MySQL8完全卸载方法-Win10系统
adb