MySQL:查询字段数量多少对查询效率的影响
这个问题是最近⼀个朋友问我的。刚好就好好看了⼀下,留下这样的记录。本⽂给出⼀些函数接⼝,末尾给出⼀些调⽤堆栈,为感兴趣的朋友做⼀个参考,也为⾃⼰做⼀个笔记。
⼀、问题由来
我们知道执⾏计划的不同肯定会带来效率的不同,但是在本例中执⾏计划完全⼀致,都是全表扫描,不同的只有字段个数⽽已。其次,测试中都使⽤了where条件进⾏过滤(Using where),过滤后没有数据返回,我们常说的where过滤实际上是在MySQL层,当然某些情况下使⽤ICP会提前在Innodb层过滤数据,这⾥我们先不考虑ICP,我会在后⾯的⽂章中详细描述ICP的流程,本⽂也会给出where过滤的接⼝,供⼤家参考。
下⾯的截图来⾃两个朋友,感谢他们的测试和问题提出。另外对于⼤数据量访问来讲可能涉及到物理IO,⾸次访问和随后的访问因为Innodb buffer的关系,效率不同是正常,需要多测试⼏次。
测试1:
测试2:
我们通过这两个测试,可以发现随着字段的不断减少,效率越来越⾼,并且主要的区别都在sending data 下⾯,这个状态我曾经⼤概描述过参考⽂章:
简单的说Innodb数据的获取和Innodb数据到MySQL层数据的传递都包含在其中。
⼆、简单的流程介绍
下⾯我主要结合字段多少和全表扫描2个⽅⾯做⼀个简单的流程介绍。实际上其中有⼀个核⼼接⼝就是
row_search_mvcc,它⼤概包含了如下功能:
通过预取缓存获取数据
打开事务
定位索引位置(包含使⽤AHI快速定位)
是否开启readview
通过持久化游标不断访问下⼀条数据
加Innodb表锁、加Innodb⾏锁
可见性判断
根据主键回表(可能回表需要加⾏锁)
ICP优化
SEMI update优化
并且作为访问数据的必须经历的接⼝,这个函数也是很值得⼤家细细研读的。
1. 通过select字段构建read_set(MySQL层)
⾸先需要构建⼀个叫做read_set的位图,来表⽰访问的字段位置及数量。它和write set⼀起,在记录binlog的Event的时候也会起着重要作⽤,可以参考我的《深⼊理解MySQL主从原理》中关于binlog_row_image参数⼀节。这⾥构建的主要接⼝为
TABLE::mark_column_used函数,每个需要访问的字段都会调⽤它来设置⾃⼰的位图。下⾯是其中的⼀段如下:
case MARK_COLUMNS_READ:
bitmap_set_bit(read_set, field->field_index);
从栈帧来看这个构建read_set的过程位于状态‘init’下⾯。栈帧见结尾栈帧1。
2. 初次访问定位的时候还会构建⼀个模板(mysql_row_templ_t)(Innodb层)
本模板主要⽤于当Innodb层数据到MySQL层做转换的时候使⽤,其中记录了使⽤的字段数量、字段的字符集、字段的类型等等。接⼝build_template_field⽤于构建这个模板。栈帧见结尾栈帧2。
但是需要注意的是,这⾥构建模板就会通过我们上⾯说的read_set去判断到底有多少字段需要构建到模板中,然后才会调⽤
build_template_field函数。如下是最重要的代码,它位于build_template_needs_field接⼝中。
bitmap_is_set(table->read_set, static_cast<uint>(i)
可以看到这⾥正在测试本字段是否出现在了read_set中,如果不在则跳过这个字段。下⾯是函数build_template_needs_field的注释:
Determines if a field is needed in a m_prebuilt struct 'template'.
@return field to use, or NULL if the field is not needed */
到这⾥我们需要访问的字段已经确⽴下来了
3. 初次定位数据,定位游标到主键索引的第⼀⾏记录,为全表扫描做好准备(Innodb层)
对于这种全表扫描的执⾏⽅式,定位数据就变得简单了,我们只需要到主键索引的第⼀条数据就好了,它和平时我们使⽤(ref/range)定位⽅式不同,不需要⼆分法的⽀持。因此对于全表扫描的初次
定位调⽤函数为btr_cur_open_at_index_side_func,⽽不是通常我们说的btr_pcur_open_with_no_init_func。
如果⼤概看⼀下函数btr_cur_open_at_index_side_func的功能,我们很容易看到,它就是通过B+树结构,定位到叶⼦结点的开头第⼀个块,然后调⽤函数page_cur_set_before_first,将游标放到了所有记录的开头,⽬的只有⼀个为全表扫描做好准备。栈帧见结尾栈帧3。注意这⾥正是通过我们row_search_mvcc调⽤下去的。
4. 获取Innodb层的第⼀条数据(Innodb层)
拿到了游标过后就可以获取数据了,这⾥也很简单代码就是⼀句如下:
rec = btr_pcur_get_rec(pcur);//获取记录从持久化游标整⾏数据
但是需要注意的是这⾥获取的数据只是⼀个指针,⾔外之意可以理解为整⾏数据,其格式也是原始的Innodb数据,其中还包含了⼀些伪列⽐如(rollback ptr和trx id)。这⾥实际上和访问的字段个数⽆关。
5. 将第⼀⾏记录转换为MySQL格式(Innodb层)
这⼀步完成后我们可以认为记录已经返回给了MySQL层,这⾥就是实际的数据拷贝了,并不是指针,整个过程放到了函数
row_sel_store_mysql_rec中。
我们前⾯的模板(mysql_row_templ_t)也会在这⾥发挥它的作⽤,这是⼀个字段过滤的过程,我们先来看⼀个循环
for (i = 0; i < prebuilt->n_template; i++)
其中prebuilt->n_template就是字段模板的个数,我们前⾯已经说过了,通过read_set的过滤,对于我们不需要的字段是不会建⽴模板的。因此这⾥的模板数量是和我们访问的字段个数⼀样的。
然后在这个循环下⾯会调⽤row_sel_store_mysql_field_func然后调⽤row_sel_field_store_in_mysql_format_func将字段⼀个⼀个转换为MySQL的格式。我们来看⼀下其中⼀种类型的转换如下:
case DATA_INT:
/* Convert integer data from Innobase to a little-endian
format, sign bit restored to normal */
ptr = dest + len;
for (;;) {
ptr--;
*ptr = *data;//值拷贝内存拷贝
if (ptr == dest) {
break;
}
data++;
}
我们可以发现这是⼀种实际的转换,也就是需要花费内存空间的。栈帧见结尾栈帧4。
到这⾥我们⼤概知道了,查询的字段越多那么着这⾥转换的过程越长,并且这⾥都是实际的内存拷贝,⽽⾮指针指向。
最终这⾏数据会存储到row_search_mvcc的形参 buffer中返回给MySQL层,这个形参的注释如下:
@param[out] buf    buffer for the fetched row in MySQL format
6.对第⼀条数据进⾏where过滤(MySQL层)
拿到数据后当然还不能作为最终的结果返回给⽤户,我们需要在MySQL层做⼀个过滤操作,这个条件⽐较位于函数evaluate_join_record 的开头,其中⽐较就是下⾯⼀句话
found= MY_TEST(condition->val_int()); //进⾏⽐较调⽤到条件和返回会记录的⽐较
如果和条件不匹配将会返回False。这⾥⽐较会最终调⽤Item_func的各种⽅法,如果等于则是Item_func_eq,栈帧见结尾栈帧5。
7.访问下⼀条数据
上⾯我已经展⽰了访问第⼀条数据的⼤体流程,接下⾯需要做的就是继续访问下去,如下:
移动游标到下⼀⾏
访问数据
根据模板转换数据返回给MySQL层
根据where条件过滤
整个过程会持续到全部主键索引数据访问完成。但是需要注意的是上层接⼝有些变化,由ha_innobase::index_first会变为
ha_innobase::rnd_next,统计数据由Handler_read_first变为Handler_read_rnd_next,这点可以参考我的⽂章:
并且row_search_mvcc的流程肯定也会有变化。这⾥不再熬述。但是实际的获取数据转换过程和过滤过程并没有改变。
mysql下载后的初次使用
注意了这些步骤除了步骤1,基本都处于sending data下⾯。
三、回到问题本⾝
好了到这⾥我们⼤概知道全表扫描的访问数据的流程了,我们就来看看⼀下在全表扫描流程中字段的多少到底有哪些异同点:
不同点:
构建的read_set 不同,字段越多read_set中为‘1’的位数越多
建⽴的模板不同,字段越多模板数量越多
每⾏数据转换为MySQL格式的时候不同,字段越多模板越多,那么循环转换每个字段的循环次数也就越多,并且这是每⾏都要处理的。
相同点:
访问的⾏数⼀致
访问的流程⼀致
where过滤的⽅式⼀致
在整个不同点中,我认为最耗时的部分应该是每⾏数据转换为MySQL格式的消耗最⼤,因为每⾏每个
字段都需要做这样的转换,这也刚好是除以sending data状态下⾯。我们线上⼤于10个字段的表⽐⽐皆是,如果我们只需要访问其中的少量字段,我们最好还是写实际的字段⽽不是‘*’,来规避这个问题。
四、写在最后
虽然本⽂中以全表扫描为列进⾏了解释,但是实际上任何情况下我们都应该缩减访问字段的数量,应该只访问需要的字段。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。