目录
MRR原理
MRR 的全称是 Multi-Range Read Optimization,是优化器将随机 IO 转化为顺序 IO 以降低查询过程中 IO 开销的一种手段。
在不使用 MRR 时,优化器需要根据二级索引返回的记录来进行“回表”,这个过程一般会有较多的随机 IO, 使用 MRR 时,SQL 语句的执行过程是这样的:
- 优化器将二级索引查询到的记录放到一块缓冲区中;
- 如果二级索引扫描到索引文件的末尾或者缓冲区已满,则使用快速排序对缓冲区中的内容按照主键进行排序;
- 线程调用 MRR 接口取 rowId,然后根据rowId 取行数据;
- 当根据缓冲区中的 rowId 取完数据,则继续调用过程 2) 3),直至扫描结束;
通过上述过程,优化器将二级索引随机的 IO 进行排序,转化为主键的有序排列,从而实现了随机 IO 到顺序 IO 的转化,提升性能。
举例
执行一个范围查询:
mysql > set optimizer_switch='mrr=off';
Query OK, 0 rows affected (0.06 sec)
mysql > explain select * from stu where age between 10 and 20;
+----+-------------+-------+-------+------+---------+------+------+-----------------------+
| id | select_type | table | type | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+----------------+------+------+-----------------------+
| 1 | SIMPLE | stu | range | age | 5 | NULL | 960 | Using index condition |
+----+-------------+-------+-------+----------------+------+------+-----------------------+
当这个 sql 被执行时,MySQL 会按照下图的方式,去磁盘读取数据(假设数据不在数据缓冲池里):
这张图是按照 Myisam 的索引结构画的,不过对于 Innodb 也同样适用。
对于 Myisam,左边就是字段 age 的二级索引,右边是存储完整行数据的地方。
先到左边的二级索引找,找到第一条符合条件的记录(实际上每个节点是一个页,一个页可以有很多条记录,这里我们假设每个页只有一条),接着到右边去读取这条数据的完整记录。
读取完后,回到左边,继续找下一条符合条件的记录,找到后,再到右边读取,这时发现这条数据跟上一条数据,在物理存储位置上,离的贼远!
咋办,没办法,只能让磁盘和磁头一起做机械运动,去给你读取这条数据。
第三条、第四条,都是一样,每次读取数据,磁盘和磁头都得跑好远一段路。
图中红色线就是整个的查询过程,蓝色线则是磁盘的运动路线。
磁盘的简化结构可以看成这样:
可以想象一下,为了执行你这条 sql 语句,磁盘要不停的旋转,磁头要不停的移动,这些机械运动,都是很费时的。
10,000 RPM 的机械硬盘,每秒大概可以执行 167 次磁盘读取,所以在极端情况下,MySQL 每秒只能给你返回 167 条数据,这还不算上 CPU 排队时间。
磁盘IOPS的计算规则
主要影响的三个参数,分别是平均寻址时间、盘片旋转速度以及最大传送速度:
第一个寻址时间,考虑到被读写的数据可能在磁盘的任意一个磁道,既有可能在磁盘的最内圈(寻址时间最短),也可能在磁盘的最外圈(寻址时间最长),所以在计算中我们只考虑平均寻址时间,也就是磁盘参数中标明的那个平均寻址时间,这里就采用当前最多的10krmp硬盘的5ms。 寻道时间Tseek是指将读写磁头移动至正确的磁道上所需要的时间。寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般在3-15ms。
第二个旋转延时,和寻址一样,当磁头定位到磁道之后有可能正好在要读写扇区之上,这时候是不需要额外额延时就可以立刻读写到数据,但是最坏的情况确实要磁盘旋转整整一圈之后磁头才能读取到数据,所以这里我们也考虑的是平均旋转延时,对于10krpm的磁盘就是(60s/10k)*(1/2) = 2ms。
第三个传送时间,磁盘参数提供我们的最大的传输速度,当然要达到这种速度是很有难度的,但是这个速度却是磁盘纯读写磁盘的速度,因此只要给定了单次 IO的大小,我们就知道磁盘需要花费多少时间在数据传送上,这个时间就是IO Chunk Size / Max Transfer Rate。(数据传输率,单位是Mb/s,兆每秒)。数据传输时间Ttransfer是指完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。目前IDE/ATA能达到133MB/s,SATA II可达到300MB/s的接口数据传输率,数据传输时间通常远小于前两部分时间。
因此,理论上可以计算出磁盘的最大IOPS,即IOPS = 1000 ms/ (Tseek + Troatation),忽略数据传输时间。假设磁盘平均物理寻道时间为3ms, 磁盘转速为7200,10K,15K rpm,则磁盘IOPS理论最大值分别为,
IOPS = 1000 / (3 + 60000/7200/2) = 140
IOPS = 1000 / (3 + 60000/10000/2) = 167
IOPS = 1000 / (3 + 60000/15000/2) = 200
开启了 MRR
很明显,要把随机访问转化成顺序访问。设置开启MRR, 重新执行 sql 语句,发现 Extra 里多了一个「Using MRR」。
mysql > set optimizer_switch='mrr=on';
Query OK, 0 rows affected (0.06 sec)
mysql > explain select * from stu where age between 10 and 20;
+----+-------------+-------+-------+------+---------+------+------+----------------+
| id | select_type | table | type | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+------+---------+------+------+----------------+
| 1 | SIMPLE | tbl | range | age | 5 | NULL | 960 | ...; Using MRR |
+----+-------------+-------+-------+------+---------+------+------+----------------+
我们开启了 MRR,重新执行 sql 语句,发现 Extra 里多了一个「Using MRR」。
这下 MySQL 的查询过程会变成这样:
在去磁盘获取完整数据之前,会先按照 rowid 排好序,再去顺序的读取磁盘。
顺序读带来了几个好处:
1、磁盘和磁头不再需要来回做机械运动;
2、可以充分利用磁盘预读;
比如在客户端请求一页的数据时,可以把后面几页的数据也一起返回,放到数据缓冲池中。这样如果下次刚好需要下一页的数据,就不再需要到磁盘读取。
这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。
3、在一次查询中,每一页的数据只会从磁盘读取一次;
MySQL 从磁盘读取页的数据后,会把数据放到数据缓冲池,下次如果还用到这个页,就不需要去磁盘读取,直接从内存读。
但是如果不排序,可能你在读取了第 1 页的数据后,会去读取第2、3、4页数据,接着你又要去读取第 1 页的数据。这时,你发现第 1 页的数据,已经从缓存中被剔除了,于是又得再去磁盘读取第 1 页的数据。
而转化为顺序读后,你会连续的使用第 1 页的数据,这时候按照 MySQL 的缓存剔除机制,这一页的缓存是不会失效的,直到你利用完这一页的数据。
由于是顺序读,在这次查询的余下过程中,你确信不会再用到这一页的数据,可以和这一页数据说告辞了。
所以,顺序读就是通过这三个方面,最大的优化了索引的读取。
拆分查询条件,进行批量查询
此外,MRR还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。
这样做的好处是可以在拆分过程中,直接过滤一些不符合查询条件的数据。
> SELECT * FROM t WHERE key_part1 >=1000 AND key_part1 < 2000 AND key_part2 = 1000;
表t有(key_part1,key_part2)的联合索引,因此索引根据key_part1,key_part2的位置关系进行排序。若没有MRR,此时查询类型为Range,SQL优化器会先将key_part1大于1000且小于2000的数据都取出来,即便key_part2不等于1000。取出后再根据key_part2的条件进行过滤。这会导致无用的数据被取出。
如果启用MRR优化器会使性能有巨大的提升,优化器会先将查询条件拆分为(1000,1000),(1001,1000),(1002,1000)....(1999,1000) 最后再根据这些拆分出的条件进行数据的查询。
使用限制
MRR 适用于range、ref、eq_ref的查询
相关参数
是否启用MRR优化,可以通过参数optimizer_switch中的flag来控制。
1、MRR 的开关:mrr =(on | off)
例如,打开MRR的开关:
mysql > set optimizer_switch='mrr=on';
2、用来告诉优化器,要不要基于使用 MRR 的成本:mrr_cost_based = (on | off)
例如,通通使用MRR:
SET GLOBAL optimizer_switch='mrr=on,mrr_cost_based=off';
考虑使用 MRR 是否值得(cost-based choice),来决定具体的 sql 语句里要不要使用 MRR。
很明显,对于只返回一行数据的查询,是没有必要 MRR 的,而如果你把 mrr_cost_based 设为 off,那优化器就会通通使用 MRR,这在有些情况下是很 stupid 的,所以建议这个配置还是设为 on,毕竟优化器在绝大多数情况下都是正确的。
3、设置用于给 rowid 排序的内存的大小:read_rnd_buffer_size,该值默认是256KB
查看配置
show VARIABLES like 'read_rnd_buffer_size';
显然,MRR 在本质上是一种用空间换时间的算法。MySQL 不可能给你无限的内存来进行排序,如果 read_rnd_buffer 满了,就会先把满了的 rowid 排好序去磁盘读取,接着清空,然后再往里面继续放 rowid,直到 read_rnd_buffer 又达到 read_rnd_buffe 配置的上限,如此循环。
没有MRR的情况下,二级索引里面得到多少行,那么就要去访问多少次主键索引(也不能完全这样说,因为MySQL实现了BNL,是把被驱动表的记录加载到内存的时候,一次性和多条驱动表中的记录做匹配,这样就可以大大减少重复从磁盘上加载被驱动表的代价),而有了MRR的时候,次数就大约减少为之前次数 t / buffer_size。
源码浅谈
mysql-5.7.25/sql/handler.cc
class DsMrr_impl
{
...
/* 利用 h2 返回的主建来查询表的全部行数据的对象 */
handler *h;
TABLE *table;
private:
/* MRR 访问二级索引 */
handler *h2;
/* MRR 执行过程中存储有序主键的缓存区,大小由 MySQL 的变量 read_rnd_buffer_size 设置. 存储 rowids, or (rowid, range_id) 对 */
uchar *rowids_buf;
uchar *rowids_buf_cur; /* 当正在读/写时的当前位置*/
uchar *rowids_buf_last; /* 当正在读时:被使用缓存空间的结尾 */
uchar *rowids_buf_end; /* 缓存空间的结尾 */
bool dsmrr_eof; /* TRUE 表示当读取索引文件已经达到 EOF了,即扫描结束*/
/*初始化和开始mrr扫描 */
int dsmrr_init(handler *h, RANGE_SEQ_IF *seq_funcs, void *seq_init_param,
uint n_ranges, uint mode, HANDLER_BUFFER *buf);
/*初始化MRR扫描*/
int handler::multi_range_read_init(RANGE_SEQ_IF *seq_funcs, void *seq_init_param, uint n_ranges, uint mode, HANDLER_BUFFER *buf);
….
/*用rowid填充缓冲区并按rowid排序*/
int dsmrr_fill_buffer();
/*
在MRR扫描中获取下一个记录
*/
int handler::multi_range_read_next(char **range_info);
/*获得DS-MRR扫描的耗费
返回值FALSE OK
返回值RUE Error, DS-MRR 不能被使用(缓存太小了,甚至连一行 rowId都存不了)
*/
bool get_disk_sweep_mrr_cost(uint keynr, ha_rows rows, uint flags, uint *buffer_size, Cost_estimate *cost);
….
}
a、MRR 中有序主建的收集过程
1)执行初始化方法dsmrr_init(...) ,优化器对查询语句的条件进行分析并选择合适的二级索引,并对二级索引的条件进行筛选拼装成要遍历的范围序列,将相关序列方法和序列参数传入初始化函数h->handler::multi_range_read_init(seq_funcs, seq_init_param, n_ranges, mode, buf) ,继而会调用 dsmrr_fill_buffer 函数;
2)在dsmrr_fill_buffer中会使用二级索引的句柄查找符合 ranges 的数据并添加至 rowids_buf 中,在扫描结束或缓冲区满的时候会对 rowids_buf 进行快速排序。
b、MRR 中主建缓冲区的使用过程
1) 物理执行阶段,调用 h->handler::multi_range_read_next(range_info);,在使用 MRR 的情况下会从过程a中收集的有序主建的缓冲区取主建,然后再调用引擎层的 h->ha_rnd_pos(table->record[0], rowid) 直接找到数据。
2) 二缓索引(h2)& 主建索引(h) 的协同是通过rowids_buf_cur来进行的。最初的初始化过程中,h2 会首先将数据填冲到 rowids_buf 中,如果发现缓冲区中的数据已经取完,则会继续调用 dsmrr_fill_buffer 往 rowids_buf 填主键并进行排序,如此反复,直至 h2 扫描至文件末尾,详情可以参考函数 DsMrr_impl::dsmrr_next。
uint mode可以是由下面标志来组合
/*
下面这两个没有被使用,因为没有对应的实现
*/
#define HA_MRR_SINGLE_POINT 1
#define HA_MRR_FIXED_KEY 2
/*
表示 RANGE_SEQ_IF::next(&range)不需要填充'range' 参数
*/
#define HA_MRR_NO_ASSOCIATION 4
/*
按输入key的顺序范围,MRR其处理的结果必须按键顺序返回行
*/
#define HA_MRR_SORTED 8
/* MRR 不需要检索完整的记录 */
#define HA_MRR_INDEX_ONLY 16
/*
内存缓冲区设置为最大限制,调用者不能承担更大的缓冲。
*/
#define HA_MRR_LIMITS 32
/*
默认MRR实现
*/
#define HA_MRR_USE_DEFAULT_IMPL 64
/*
保证扫描范围的边界将没有NULL值。
*/
#define HA_MRR_NO_NULL_ENDPOINTS 128
下面为一些重要方法的具体实现
/**ds-mrr初始化和开始mrr扫描 */
int DsMrr_impl::dsmrr_init(handler *h_arg, RANGE_SEQ_IF *seq_funcs,
void *seq_init_param, uint n_ranges, uint mode,
HANDLER_BUFFER *buf)
{
uint elem_size;
int retval= 0;
DBUG_ENTER("DsMrr_impl::dsmrr_init");
THD *thd= h_arg->table->in_use;
h= h_arg;
// DS-MRR 不进行排序,我们可以通过OPTIMIZER_SWITCH_MRR参数来控制
if (!hint_key_state(thd, h->table, h->active_index,
MRR_HINT_ENUM, OPTIMIZER_SWITCH_MRR) ||
mode & (HA_MRR_USE_DEFAULT_IMPL | HA_MRR_SORTED))
{
use_default_impl= TRUE;
retval= h->handler::multi_range_read_init(seq_funcs, seq_init_param,
n_ranges, mode, buf);
DBUG_RETURN(retval);
}
/*
如果已经将索引条件压入主键索引对象,然后改变主意,使用不同的索引进行MRR检索数据。以下标准之一必须是真实的:
1. 我们没有在这个处理程序上推入索引条件。
2. 我们已经推入了一个索引条件,这是当前使用的索引。
3. 我们已经推入了一个索引条件,但这不是针对主键的。
4. 我们已经推入了一个索引条件,这个已经被转移到了处理程序对象的克隆(h2)。
*/
DBUG_ASSERT(!h->pushed_idx_cond ||
h->pushed_idx_cond_keyno == h->active_index ||
h->pushed_idx_cond_keyno != table->s->primary_key ||
(h2 && h->pushed_idx_cond_keyno == h2->active_index));
rowids_buf= buf->buffer;
is_mrr_assoc= !MY_TEST(mode & HA_MRR_NO_ASSOCIATION);
if (is_mrr_assoc)
table->in_use->status_var.ha_multi_range_read_init_count++;
rowids_buf_end= buf->buffer_end;
elem_size= h->ref_length + (int)is_mrr_assoc * sizeof(void*);
rowids_buf_last= rowids_buf +
((rowids_buf_end - rowids_buf)/ elem_size)*
elem_size;
rowids_buf_end= rowids_buf_last;
/*
DS-MRR扫描使用第二个处理程序对象(h2)来完成索引扫描。
通过克隆primary handler object来创建。
当DsMrr_impl::reset()执行时,h2会被删除。
*/
if (!h2)
{
handler *new_h2;
if (check_stack_overrun(thd, 5*STACK_MIN_SIZE, (uchar*) &new_h2))
DBUG_RETURN(1);
if (!(new_h2= h->clone(h->table->s->normalized_path.str, thd->mem_root)))
DBUG_RETURN(1);
h2= new_h2;
table->prepare_for_position();
}
/*
MAX_KEY 表示 Max used keys
h2 使用 primary handler的key进行索引扫描
*/
if (h2->active_index == MAX_KEY)
{
DBUG_ASSERT(h->active_index != MAX_KEY);
const uint mrr_keyno= h->active_index;
if ((retval= h2->ha_external_lock(thd, h->m_lock_type)))
goto error;
if ((retval= h2->extra(HA_EXTRA_KEYREAD)))
goto error;
if ((retval= h2->ha_index_init(mrr_keyno, false)))
goto error;
// 从 h 到 h2 转移 ICP(索引下推)
if (mrr_keyno == h->pushed_idx_cond_keyno)
{
if (h2->idx_cond_push(mrr_keyno, h->pushed_idx_cond))
{
retval= 1;
goto error;
}
}
else
{
// 取消之前任何潜在的索引条件推送
h2->cancel_pushed_idx_cond();
}
}
else
{
/*
h2 已经是一个 open index. 这种情况出现在当第一次重新开启时没有关闭它。
在这种情况下,因为从表中读取数据,primary handler必须被使用
在做新的范围扫描时, h2不能被打开。
在这种情况下,primary handler的active_index既不能被设置,也不能为primary key.
*/
DBUG_ASSERT(h->inited == handler::RND);
DBUG_ASSERT(h->active_index == MAX_KEY ||
h->active_index == table->s->primary_key);
}
/*
现在索引扫描已经转移到 h2了,我们可以关闭 primary handler 的
open index scan
*/
if (h->inited == handler::INDEX)
{
/*
调用 h->ha_index_end()时,会再调用 dsmrr_close(),其将关闭h2的索引扫描。
我们需要保持h2打开,所以进行临时性移除
*/
handler *save_h2= h2;
h2= NULL;
retval= h->ha_index_end();
h2= save_h2;
if (retval)
goto error;
}
/*
验证一致性 between h and h2.
*/
DBUG_ASSERT(h->inited != handler::INDEX);
DBUG_ASSERT(h->active_index == MAX_KEY ||
h->active_index == table->s->primary_key);
DBUG_ASSERT(h2->inited == handler::INDEX);
DBUG_ASSERT(h2->active_index != MAX_KEY);
DBUG_ASSERT(h->m_lock_type == h2->m_lock_type);
if ((retval= h2->handler::multi_range_read_init(seq_funcs, seq_init_param,
n_ranges, mode, buf)))
goto error;
if ((retval= dsmrr_fill_buffer()))
goto error;
...
}
/*初始化MRR扫描*/
int handler::multi_range_read_init(RANGE_SEQ_IF *seq_funcs, void *seq_init_param,
uint n_ranges, uint mode, HANDLER_BUFFER *buf)
{
DBUG_ENTER("handler::multi_range_read_init");
mrr_iter= seq_funcs->init(seq_init_param, n_ranges, mode);
mrr_funcs= *seq_funcs;
mrr_is_output_sorted= MY_TEST(mode & HA_MRR_SORTED);
mrr_have_range= FALSE;
DBUG_RETURN(0);
}
/*用rowid填充缓冲区并按rowid排序*/
int DsMrr_impl::dsmrr_fill_buffer()
{
char *range_info;
int res= 0;
DBUG_ENTER("DsMrr_impl::dsmrr_fill_buffer");
DBUG_ASSERT(rowids_buf < rowids_buf_end);
/*
将key_read设置为TRUE,因为我们只从索引中读取字段,这也确保了任何虚拟列都是从索引中读取,而不是试图从基列进行读取
*/
DBUG_ASSERT(table->key_read == FALSE);
table->key_read= TRUE;
rowids_buf_cur= rowids_buf;
while ((rowids_buf_cur < rowids_buf_end) &&
!(res= h2->handler::multi_range_read_next(&range_info)))
{
KEY_MULTI_RANGE *curr_range= &h2->handler::mrr_cur_range;
/*检查记录组合是否匹配索引条件*/
if (h2->mrr_funcs.skip_index_tuple &&
h2->mrr_funcs.skip_index_tuple(h2->mrr_iter, curr_range->ptr))
continue;
/* 把 rowid 或者 {rowid, range_id} 对加入 buffer */
h2->position(table->record[0]);
memcpy(rowids_buf_cur, h2->ref, h2->ref_length);
rowids_buf_cur += h2->ref_length;
if (is_mrr_assoc)
{
memcpy(rowids_buf_cur, &range_info, sizeof(void*));
rowids_buf_cur += sizeof(void*);
}
}
// 恢复key_read,因为下一个读操作将读取完整的行
table->key_read= FALSE;
/* HA_ERR_END_OF_FILE 表示 No rows in range*/
if (res && res != HA_ERR_END_OF_FILE)
DBUG_RETURN(res);
dsmrr_eof= MY_TEST(res == HA_ERR_END_OF_FILE);
uint elem_size= h->ref_length + (int)is_mrr_assoc * sizeof(void*);
size_t n_rowids= (rowids_buf_cur - rowids_buf) / elem_size;
/* 通过rowId对缓存内容进行排序 */
my_qsort2(rowids_buf, n_rowids, elem_size, (qsort2_cmp)rowid_cmp,
(void*)h);
rowids_buf_last= rowids_buf_cur;
rowids_buf_cur= rowids_buf;
DBUG_RETURN(0);
}
/*
在MRR扫描中获取下一个记录
*/
int handler::multi_range_read_next(char **range_info)
{
int result= HA_ERR_END_OF_FILE;
int range_res;
/**
一些非虚拟的ha_*函数,负责读取行,与ha_rnd_pos()类似,必须确保虚拟生成的变量在他们返回之前就算好了。为此,他们应该设置这个成员在开始时设置为true,并在返回前进行检查:
如果成员仍然是true,这意味着他们应该计算;如果它是false,这意味着已经被一些更低层次的方法计算处理了,不需要重新做(这就是为什么我们需要这个状态标志:为性能,以避免冗余计算)
*/
m_update_generated_read_fields= table->has_gcol();
/*TRUE 表示我们目前正在遍历mrr_cur_range中的一个范围*/
if (!mrr_have_range)
{
mrr_have_range= TRUE;
goto start;
}
do
{
if (mrr_cur_range.range_flag != (UNIQUE_RANGE | EQ_RANGE))
{
result= read_range_next();
/* 在 success or non-EOF 错误时跳出循环 */
if (result != HA_ERR_END_OF_FILE)
break;
}
else
{
/*
在UPDATE或DELETE中,如果游标下的行被另一个事务行锁定,存储引擎在游标下使用最后一个乐观读取已提交的行值,然后引擎从中返回1函数。这可以用来避免不必要的锁等待。
如果该方法返回非零值,它将通知存储引擎的下一个读取将是对该行的锁定重新读取。
*/
if (was_semi_consistent_read())
goto scan_it_again;
}
start:
/*
尝试获取下一个 range(s), 直到匹配上一行记录
*/
while (!(range_res= mrr_funcs.next(mrr_iter, &mrr_cur_range)))
{
scan_it_again:
result= read_range_first(mrr_cur_range.start_key.keypart_map ?
&mrr_cur_range.start_key : 0,
mrr_cur_range.end_key.keypart_map ?
&mrr_cur_range.end_key : 0,
MY_TEST(mrr_cur_range.range_flag &EQ_RANGE),
mrr_is_output_sorted);
if (result != HA_ERR_END_OF_FILE)
break;
}
}
while ((result == HA_ERR_END_OF_FILE) && !range_res);
*range_info= mrr_cur_range.ptr;
/* 更新虚拟生成变量*/
if (!result && m_update_generated_read_fields)
{
result= update_generated_read_fields(table->record[0], table, active_index);
m_update_generated_read_fields= false;
}
DBUG_PRINT("exit",("handler::multi_range_read_next result %d", result));
DBUG_RETURN(result);
}
/*获得DS-MRR扫描的耗费
FALSE OK
TRUE Error, DS-MRR 不能被使用(缓存太小了,甚至连一行 rowId都存不了)
*/
bool DsMrr_impl::get_disk_sweep_mrr_cost{
ha_rows rows_in_last_step;
uint n_full_steps;
const uint elem_size= h->ref_length +
sizeof(void*) * (!MY_TEST(flags & HA_MRR_NO_ASSOCIATION));
const ha_rows max_buff_entries= *buffer_size / elem_size;
if (!max_buff_entries)
return TRUE; /* DS-MRR 不能被使用(缓存太小了,甚至连一行 rowId都存不了) */
/* rows可以full buffer的次数*/
n_full_steps= (uint)floor(rows2double(rows) / max_buff_entries);
/*处理最后一次迭代时(non-full buffer)的行数 */
rows_in_last_step= rows % max_buff_entries;
DBUG_ASSERT(cost->is_zero());
if (n_full_steps)
{
/*计算一次排序和扫描的消耗*/
get_sort_and_sweep_cost(table, max_buff_entries, cost);
cost->multiply(n_full_steps);
}
else
{
/*
调整缓冲区大小,因为只有部分缓冲区将被使用:
*/
const ha_rows keys_in_buffer=
max<ha_rows>(static_cast<ha_rows>(1.2 * rows_in_last_step), 100);
/*2. 如果估计所需的缓冲区大小小于建议的,调用者将其设置为估计的缓冲区大小。*/
*buffer_size= min<ulong>(*buffer_size,
static_cast<ulong>(keys_in_buffer) * elem_size);
}
Cost_estimate last_step_cost;
get_sort_and_sweep_cost(table, rows_in_last_step, &last_step_cost);
(*cost)+= last_step_cost;
/*
内存消耗暂时不计算在 total_cost() function ,未来会加入。
*/
cost->add_mem(*buffer_size);
/* 所有索引访问的total cost */
(*cost)+= h->index_scan_cost(keynr, 1, static_cast<double>(rows));
/*
增加处理记录的cpu消耗(详情看 @handler::multi_range_read_info_const()).
*/
cost->add_cpu(table->cost_model()->row_evaluate_cost(
static_cast<double>(rows)));
return FALSE;
}
/*计算排序记录行数的消耗*/
static void get_sort_and_sweep_cost(TABLE *table, ha_rows nrows, Cost_estimate *cost)
{
if (nrows)
{
/*读取记录行的消耗*/
get_sweep_read_cost(table, nrows, FALSE, cost);
const double ROWID_COMPARE_SORT_COST=
table->cost_model()->key_compare_cost(1.0) / 10;
// cpu_sort= nrows * log2(nrows) * table->cost_model()->rowid_compare_cost();
const double cpu_sort= nrows * log2(nrows) * ROWID_COMPARE_SORT_COST;
cost->add_cpu(cpu_sort);
}
}