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 时,优化器需要根据二级索引返回的记录来进行“回表”,这个过程一般会有较多的随机 IO, 使用 MRR 时,SQL 语句的执行过程是这样的:
通过上述过程,优化器将二级索引随机的 IO 进行排序,转化为主键的有序排列,从而实现了随机 IO 到顺序 IO 的转化,提升性能。
首先,咱们来看一下 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 是如何实现的。
场景A:对于InnoDB和MyISAM表的索引范围扫描和等值连接操作,可以使用MRR优化。
执行流程:
场景举例:
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 的
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,毕竟优化器在绝大多数情况下都是正确的。