其他分享
首页 > 其他分享> > 不是吧!阿sir!竟然有比B+Tree更快的一种查询结构!

不是吧!阿sir!竟然有比B+Tree更快的一种查询结构!

作者:互联网

我们都知道MySQL中的B+Tree索引结构,对于根据某个条件查找记录是非常快的。那么,在不断追求极致的驱动下,你有没有想过MySQL会不会有比B+Tree更快的数据结构,来加速查找记录的性能呢?答案是有的,MySQL为了让我们更快地获取自己想查找的记录,在InnoDB中,将查询频繁的条件和索引树结果做了一个Hash映射,这样,一个查询就不需要每次搜索B+Tree去定位结果了,这个Hash映射就叫做AHI,全称Adaptive Hash Index,自适应哈希索引

一听这名字,你或许已经猜出个一二了。没错!它其实就是一个HashTable,在大学学习《数据结构》的时候,我们都知道Hash Table在查找其中的一个节点的数据是非常快的,算法时间复杂度O(1),所以,相比B+Tree而言,它的查找性能一定是更快的。

但是,有个问题:为什么这个Hash Table叫做自适应哈希索引呢,这个“自适应”是什么概念?

从下面这个案例开始,详细讲解AHI,逐步带你明白AHI这个自适应是怎么一回事?

假设我们交友平台有个功能:筛选出年龄在15到23之间的用户。那么,通常我们会用下面这条SQL实现:

SELECT id, age, sex FROM user WHERE age >= 15 AND age <= 23

同时,我们给user表建了一个索引index_age_sex(age,sex),那么,现在我们再来看看这条SQL是如何使用AHI的?

AHI

既然AHI也是一个HashTable,那首先,你肯定会关心,它的Key是什么样的,Value又是什么样的?那么,我们就先来看看AHI的Key和Value。

我们看到《导读》中的语句的查询条件为age >= 15 AND age <= 23,按照上面我说的AHI的含义:将某一个查询条件和其结果做了一个Hash映射,那么,我们想象中的这个HashTable就类似下面这样:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

图中上面的age >= 15 AND age <= 23代表查询条件,也就Key,下面为索引index_age_sex中满足查询条件的4条记录,也就是Value。其中,每条记录的结构为[age,sex,id]。

Key

但是,从上面的图来看,如果查询条件的字段很长,那么,Key存储的空间也就变得很大,对于MySQL这种内存敏感的系统而言肯定是不能接受的,因此,MySQL设计了下面的这种结构来存放Key:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

如上图为查找索引index_age_sex时,查询条件为age >= 15 AND age <= 23的结构:

最终,MySQL通过search_info::n_fields和dtuple_t的组合来表达查询条件age >= 15 AND age <= 23。如上图中的两个箭头表示的就是这种组合。

讲完Key,我们再来看看MySQL是如何设计HashTable的Value的?

Value

当然,如果按照上面的HashTable的结构,我们肯定认为查询条件age >= 15 AND age <= 23,其在HashTable中的Value就是上面图中1-1中的下面的记录。但是,我们现在来看下面一个场景:

假设现在我将查询条件变为age >= 15 AND age < 16,那么,这个HashTable就变成这样:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

图中上面的age >= 15 AND age < 16代表查询条件,下面为索引index_age_sex中满足查询条件的2条记录,其中,每条记录的结构为[age,sex,id]。

通过对比1-1和1-2-1上面两张图,我们发现2个查询条件对应的查询结果中有重复记录15,0,2和15,0,5。现在只有2个查询条件会出现重复记录,那么,如果将来有几十个,甚至上百个查询条件都包含重复记录,那么,是不是在存储空间上就很浪费了?

因此,为了节省查询结果的存储空间,我们可以将上面2个查询HashTable合并,变成下面这样的结构:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

图中MySQL将条件age >= 15 AND age <= 23和条件age >= 15 AND age < 16对应的记录合并为4条:

但是,在讲述Key的结构时,我说了MySQL真实设计的Key结构如图1-1-1,对应到图1-2-2,显然图1-2-2中的Key不是MySQL真实存储的结构。那么,结合1-2-2的HashTable概念图,我们来看下MySQL到底是如何设计AHI的Key和Value的映射的?

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

如上图,是MySQL完整的AHI的存储结构。其中,Value上面的部分就是Key,上面我已经讲解过Key的结构,这里我就不再重述了。我们主要看下Value部分:

现在我们知道了AHI的完整结构,通过这个结构,我们发现MySQL没有直接将查询条件和结果做了映射,而是通过cell将条件和结果关联起来,这样做的好处就是相同条件边界值对应的node在内存中可以共享,节省了存储空间。

查询AHI

说了那么多,是不是发现好像这个AHI结构并没有完整存储查询条件对应的所有结果记录,(毕竟我要的可是4条满足条件的记录哦!),那MySQL又是怎么通过AHI找到所有满足条件的记录呢?下面我们就以age >= 15 AND age < 16这个查询条件为例,来看一下这个查找过程:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

  1. 根据条件左边界值15,做hash运算,计算得到一个fold值,通过该值定位到第1个cell。
  2. 遍历第1个cell下的node,找到第1个node为边界值15对应的node。
  3. 根据第1个node找到对应的记录<15,0,2>、page(10)和curr_left_side=true。
  4. 根据上一步得到的page编号10和记录,在索引树index_age_sex中找到10号叶子节点中的记录<15,0,2>。
  5. 根据条件右边界值16,做hash运算,计算得到一个fold值,通过该值定位到第2个cell。
  6. 遍历第2个cell下的node,找到第1个node为边界值16对应的node。
  7. 根据第1个node找到对应的记录<16,0,3>、page(11)和curr_left_side=false。
  8. 根据上一步得到的page编号11和记录,在索引树index_age_sex找到11号叶子节点中的记录<16,0,3>。
  9. 由于第3步中记录<15,0,2>所在node中curr_left_side=true,说明记录<15,0,2>为查询条件最左记录,因此,从索引树index_age_sex的10号叶子节点内<15,0,2>记录开始,向后遍历其他记录。
  10. 由于第7步中记录<16,0,3>所在node中curr_left_side=false,说明记录<16,0,3>为查询条件最右记录,故上一步遍历到记录<16,0,3>结束。
  11. 最终,在索引树index_age_sex中找到所有满足条件age >= 15 AND age < 16的记录。

其中,第4,8 ~ 11步的细节过程,可以参考文章《InnoDB是顺序查找B+Tree叶子节点的吗?》

构建AHI时机

现在我们知道了MySQL如何通过AHI找到满足条件的记录了,那么,这个AHI又是在什么时候创建的,如何创建的呢?

在《导读》中我讲过,MySQL对使用频繁的查询条件才构建AHI,即条件与结果的映射关系。因此,我们就要看看MySQL是如何判断这个查询条件是否频繁使用的?

为了统计一个条件使用的频率,MySQL设计了下面这样一种结构。

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

是不是有点眼熟?其实,图中search_info就是查询信息的结构,在图1-1-1中,我讲了search_info中的一个属性n_fields,现在,我再讲另一个属性hash_analysis。

当一次查询成功后,MySQL通过累加该属性,记录该次查询成功的次数。比如,初始hash_analysis=0,那么,条件age >= 15 AND age < 16查询成功一次,hash_analysis + 1 = 1,再成功一次,hash_analysis + 1 = 2,依次类推,成功多少次,hash_analysis就是多少。

当hash_analysis值超过17时,MySQL就会对该查询构建AHI。

但是,查询成功,就一定能够构建AHI吗?答案是不一定!我们来看下面这个场景:

SELECT age, sex FROM user WHERE age >= 15 AND age <= 18

上面这条语句,MySQL在索引index_age_sex中的叶子节点找到满足条件的记录为下面4条:

<15,0,2>`、`<16,0,3>`、`<18,0,4>`、`<18,0,5>

这时候,我们再看下这个条件查找AHI的过程:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

  1. 根据条件左边界值15,做hash运算,计算得到一个fold值,通过该值定位到第1个cell。
  2. 遍历第1个cell下的node,找到第1个node为边界值15对应的node。
  3. 根据得到的node找到对应的记录<15,0>、page(10)和curr_left_side=true。
  4. 根据上一步得到的page编号10和记录,在索引树index_age_sex中找到10号叶子节点中满足node记录<15,0>的第一条记录<15,0,2>。
  5. 根据条件右边界值18,做hash运算,计算得到一个fold值,通过该值定位到第2个cell。
  6. 遍历第2个cell下的node,找到第1个node为边界值18对应的node。
  7. 根据得到的node找到对应的记录<18,0>、page(11)和curr_left_side=false。
  8. 根据上一步得到的page编号11和记录,在索引树index_age_sex找到11号叶子节点中满足node记录<18,0>的第一条记录<18,0,4>。
  9. 由于第3步中记录<15,0>所在node中curr_left_side=true,说明记录<15,0,2>为查询条件最左记录,因此,从索引树index_age_sex的10号叶子节点内<15,0,2>记录开始,向后遍历其他记录。
  10. 由于第7步中记录<18,0>所在node中curr_left_side=false,说明记录<18,0,4>为查询条件最右记录,故上一步遍历到记录<18,0,4>结束。

其中,第4,8 ~ 10步的细节过程,可以参考文章《InnoDB是顺序查找B+Tree叶子节点的吗?》

从上面的过程,我们发现一个问题:明明11号叶子节点中的记录<18,0,5>也满足条件age >= 15 AND age <= 18,但是,AHI查询却忽略这条记录。如上图,虚线标出的记录。

因此,我们发现并不是所有的查询都支持AHI,我们不能简单地认为只要查询成功,就等于可以构建AHI。

为此,MySQL在search_info中引入了一个新的属性,我们来看下:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

如上图中的n_hash_potential就是这个新属性,它表示一次查询潜在可以成功构建AHI的次数。用它来解决上面那个场景的问题:

只有查询得到的结果中,最大的记录中的select字段值唯一,n_hash_potential才会累加。

这样一来,MySQL就在真正构建AHI之前做了两次拦截:

那么,下一个问题来了:既然我都已经知道上面那个场景是不可能构建AHI的,我为什么还要让查询处理进入上面两次拦截检查呢?

因此,为了避免进入上面的拦截检查,MySQL又在search_info中引入了一个属性:

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

图中last_hash_succ属性,它表示上一次是否成功构建AHI。

有了这个属性,MySQL只要发现上面这个场景压根er不能构建AHI,因此,直接就设置last_hash_succ=false,那么,在下次相同查询进来后,直接发现last_hash_succ=false,就不再进行后面两次的拦截检查。

通过上面的分析,我们就得出了一次查询触发AHI构建的检查过程:

如果last_hash_succ=false,该查询不能构建AHI,反之进入下一步检查

如果hash_analysis < 17,该查询不能构建AHI,反之进入下一步检查

如果n_hash_potential < 100,该查询不能构建AHI,反之可以构建AHI

构建AHI

讲完AHI构建的触发条件,我们最后来看看MySQL是如何构建AHI的?

通过《查询AHI》部分的讲解,我们知道查询AHI的过程中,AHI中的Node中包含几个核心元素block、left_side和page,因此,我们只要知道这几个核心元素是如何构建的,也就能够描述清楚AHI的构建过程了。

我以下面这条语句为例,看下AHI构建的过程:

SELECT id, age, sex FROM user WHERE age >= 15 AND age <= 18

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

关注图中红线部分:

  1. 根据条件左边界值15,在索引树index_age_sex中的10号叶子节点中找到满足边界值的第一条记录<15,0,2>。
  2. 由于找到满足左边界值15的记录只有一条,因此,MySQL将up_match + 1 = 1,表示只有一条记录满足左边界值。由于up_match > low_match,因此,search_info中的left_side设置为true。
  3. 根据条件左边界值15,对其做hash运算,定位到AHI中的第1个cell。
  4. 发现cell中没有node节点,创建一个node。即图中灰色的node节点。
  5. 在node中创建一个block。如上图浅蓝色的block。
  6. 将索引树index_age_sex中的10号叶子节点信息写入block中的page属性。
  7. 将第2步得到的left_side写入block中的curr_left_side。
  8. 将第1步得到的记录<15,0,2>写入node。
  9. 同理,条件右边界值18构建AHI的过程相同。

AHI锁

了解完AHI的构建过程后,我们进一步会想,如果并发构建AHI,会出现node覆盖的问题。因此,为了解决这个问题,MySQL就必须给AHI加一把锁,避免并发构建时产生node覆盖的问题。

当然,我们不能给整个AHI加全局锁吧,因为这样会非常影响查询的性能,因此,MySQL是这样设计锁的。

不是吧!不是吧!竟然有比B+Tree更快的一种查询结构

MySQL在启动时,从innodb_buffer_pool中划分出若干个Hash Table,作为AHI,图中,我画了2个HashTable:HashTable[0]和HashTable[1]。假设MySQL通过4个查询条件hash运算得到4个fold,如上图4个fold值分别为1、2、9和17。

那么,MySQL对AHI加锁的方式为fold % 8取模:

通过这种方式,MySQL就可以将锁分散加在不同的HashTable上,尽可能减少并发导致的HashTable构建锁死造成的性能问题。

小结

本章中,小k详细讲解AHI的结构、查询、构建、加锁等原理。

现在回答文章开头的问题:为什么MySQL把这个HashTable叫做自适应哈希索引呢?

通过AHI构建的过程,我们发现,多个查询构建cell中的node,是可以变大或缩小的,正是这个原因,MySQL才把这样一个HashTable叫做AHI,即自适应哈希索引。

思考题

最后留一个思考题:在文中《构建AHI》部分中,我有提到up_match和low_match属性,我们明明可以通过查询条件是>或<来判断left_side为true还是false,为什么还要通过up_match和low_match来判断呢?

提示:结合索引多列查询场景思考一下。

最后,希望你有所收获,如果你觉得文章还不错,记得点赞哦~~

需要资料的小伙伴,点赞加收藏,关注我之后添加小助理vx:bjmsb0606006 即可获取免费下载方式

标签:node,sir,15,age,AHI,Tree,查询,更快,MySQL
来源: https://blog.csdn.net/Fightevery/article/details/118725354