水平有限如有误请指出
源码版本:5.7.22
这个问题是最近一个朋友问我的。刚好就好好看了一下,留下这样的记录。本文给出一些函数接口,末尾给出一些调用堆栈,为感兴趣的朋友做一个参考,也为自己做一个笔记。
一、问题由来
我们知道执行计划的不同肯定会带来效率的不同,但是在本例中执行计划完全一致,都是全表扫描,不同的只有字段个数而已。其次,测试中都使用了where条件进行过滤(Using where),过滤后没有数据返回,我们常说的where过滤实际上是在MySQL层,当然某些情况下使用ICP会提前在层过滤数据,这里我们先不考虑ICP,我会在后面的文章中详细描述ICP的流程,本文也会给出where过滤的接口,供大家参考。
下面的截图来自两个朋友,感谢他们的测试和问题提出。另外对于大数据量访问来讲可能涉及到物理IO,首次访问和随后的访问因为 的关系,效率不同是正常,需要多测试几次。
测试1:
p1.png
p2.png
p3.png
测试2:
p4.png
我们通过这两个测试,可以发现随着字段的不断减少,效率越来越高,并且主要的区别都在 data 下面,这个状态我曾经大概描述过参考文章:
简单的说数据的获取和数据到MySQL层数据的传递都包含在其中。
二、简单的流程介绍
下面我主要结合字段多少和全表扫描2个方面做一个简单的流程介绍。实际上其中有一个核心接口就是,它大概包含了如下功能:
并且作为访问数据的必须经历的接口,这个函数也是很值得大家细细研读的。
1. 通过字段构建(MySQL层)
首先需要构建一个叫做的位图,来表示访问的字段位置及数量。它和write set一起,在记录的Event的时候也会起着重要作用,可以参考我的《深入理解MySQL主从原理》中关于参数一节。这里构建的主要接口为TABLE::函数,每个需要访问的字段都会调用它来设置自己的位图。下面是其中的一段如下:
case MARK_COLUMNS_READ:
bitmap_set_bit(read_set, field->field_index);
从栈帧来看这个构建的过程位于状态‘init’下面。栈帧见结尾栈帧1。
2. 初次访问定位的时候还会构建一个模板()(层)
本模板主要用于当层数据到MySQL层做转换的时候使用,其中记录了使用的字段数量、字段的字符集、字段的类型等等。接口用于构建这个模板。栈帧见结尾栈帧2。
但是需要注意的是,这里构建模板就会通过我们上面说的去判断到底有多少字段需要构建到模板中,然后才会调用函数。如下是最重要的代码,它位于接口中。
bitmap_is_set(table->read_set, static_cast(i)
可以看到这里正在测试本字段是否出现在了中,如果不在则跳过这个字段。下面是函数的注释:
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. 初次定位数据,定位游标到主键索引的第一行记录,为全表扫描做好准备(层)
对于这种全表扫描的执行方式,定位数据就变得简单了,我们只需要找到主键索引的第一条数据就好了,它和平时我们使用(ref/range)定位方式不同,不需要二分法的支持。因此对于全表扫描的初次定位调用函数为,而不是通常我们说的。
如果大概看一下函数的功能,我们很容易看到,它就是通过B+树结构,定位到叶子结点的开头第一个块,然后调用函数first,将游标放到了所有记录的开头,目的只有一个为全表扫描做好准备。栈帧见结尾栈帧3。
注意这里正是通过我们调用下去的。
4. 获取层的第一条数据(层)
拿到了游标过后就可以获取数据了,这里也很简单代码就是一句如下:
rec = btr_pcur_get_rec(pcur);//获取记录 从持久化游标 整行数据
但是需要注意的是这里获取的数据只是一个指针,言外之意可以理解为整行数据,其格式也是原始的数据,其中还包含了一些伪列比如( ptr和trx id)。这里实际上和访问的字段个数无关。
5. 将第一行记录转换为MySQL格式(层)
这一步完成后我们可以认为记录已经返回给了MySQL层,这里就是实际的数据拷贝了,并不是指针,整个过程放到了函数rec中。
我们前面的模板()也会在这里发挥它的作用,这是一个字段过滤的过程,我们先来看一个循环
for (i = 0; i n_template; i++)
其中->就是字段模板的个数,我们前面已经说过了,通过的过滤,对于我们不需要的字段是不会建立模板的。因此这里的模板数量是和我们访问的字段个数一样的。
然后在这个循环下面会调用然后调用将字段一个一个转换为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。
到这里我们大概知道了,查询的字段越多那么着这里转换的过程越长,并且这里都是实际的内存拷贝,而非指针指向。
最终这行数据会存储到的形参 中返回给MySQL层,这个形参的注释如下:
@param[out] buf buffer for the fetched row in MySQL format
6.对第一条数据进行where过滤(MySQL层)
拿到数据后当然还不能作为最终的结果返回给用户,我们需要在MySQL层做一个过滤操作,这个条件比较位于函数的开头,其中比较就是下面一句话
found= MY_TEST(condition->val_int()); //进行比较 调用到 条件和 返回会记录的比较
如果和条件不匹配将会返回False。这里比较会最终调用的各种方法,如果等于则是,栈帧见结尾栈帧5。