MySQL 优化器 MRR

什么是 MRR

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

其中表结构如下:

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)

操作如下:

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 打开,执行以下操作:

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 相对应的内存结构:

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,其调用堆栈下:

 #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 的调用堆栈如下:

 #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列创建了一个索引。当执行以下查询时:

SELECT order_id FROM orders WHERE order_date BETWEEN '2022-01-01' AND '2022-12-31';

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

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

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列创建了一个多范围索引。当执行以下查询时:

SELECT product_id FROM products WHERE price BETWEEN 10 AND 100;

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

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

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

SELECT order_id FROM orders INNER JOIN customers ON orders.customer_id = customers.customer_id;

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

MRR 如何使用

//如果你不打开,是一定不会用到 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,毕竟优化器在绝大多数情况下都是正确的。

你可能感兴趣的:(MySQL,查询优化,adb)