MySQL索引的底层原理与B+树的由来(让你明白的不能再明白)

1,079 阅读9分钟

背景

小伙伴有没有这种感觉,在用数据库的时候,我们都知道索引能加快我们对数据库的查找速度,但就是不知道具体原因是什么,看了很多技术文章的解释,也总感觉很模糊似的。

希望读了这篇文章后,对你有所收获!

正文

一、为什么数据库要使用索引

从磁盘的角度来说,建立索引就是在减少磁盘I/O的次数,那么先让我们来看看磁盘的I/O过程: 图片是一个盘面,盘面中一圈圈灰色同心圆为一条条磁道,从圆心向外画直线,可以将磁道划分为若干个弧段,每个磁道上一个弧段被称之为一个扇区:

  • 扇区:是磁盘存放数据的最小组成单元,通常的大小是512字节。(根据磁盘品牌不同,扇区的大小也有不同)
  • 磁道:将扇区和扇区之间隔开的弧段。

这里的扇区是物理上的扇区,由于不同品牌的物理扇区不同,导致过大的物理扇区会导致系统和软件的兼容性问题,有了逻辑扇区的概念,物理扇区每次都会向操作系统和应用软件提供相应的物理扇区报告,让其实现物理扇区和逻辑扇区的转换,所以,我们平时所称的扇区即为逻辑扇区(也称块,即block,所以小伙伴们注意喽!下文我所提到的扇区指的就是逻辑扇区。)

回到正题,我们都知道对MySQL加索引就是为了减少IO时间;那为什么减少IO时间就能提升性能呢?

要想搞清这个问题,根据上面对磁盘的简单介绍,我们来看看磁盘如何完成单次IO操作的,共分为三步:

  (1) 寻道

  当控制器对磁盘发出一个IO操作命令的时候,磁盘的驱动臂(Actuator Arm)带动磁头(Head)离开着陆区(Landing Zone,位于内圈没有数据的区域),移动到要操作的初始数据块所在的磁道(Track)的正上方,这个过程被称为寻道(Seeking),对应消耗的时间被称为寻道时间(Seek Time);

  (2) 旋转延迟

  找到对应磁道还不能马上读取数据,这时候磁头要等到磁盘盘片(Platter)旋转到初始数据块所在的扇区(Sector)落在读写磁头正下方之后才能开始读取数据,在这个等待盘片旋转到可操作扇区的过程中消耗的时间称为旋转延时(Rotational Latency);

  (3) 数据传送

  接下来就随着盘片的旋转,磁头不断的读/写相应的数据块,直到完成这次IO所需要操作的全部数据,这个过程称为数据传送(Data Transfer),对应的时间称为传送时间(Transfer Time)。完成这三个步骤之后单次IO操作也就完成了。

  根据磁盘单次IO操作的过程,可以发现:

  单次IO时间 = 寻道时间 + 旋转延迟 + 传送时间

  进而推算IOPS(IO per second)的公式为:

  IOPS = 1000ms/单次IO时间

由上述的IO操作可知,磁盘为了寻找扇区,做了寻道和旋转延迟操作,而这两步的操作所花费时间占比是巨大的。由此可见,减少所要操作的扇区个数,进而就能减少寻道和旋转延迟操作次数,是节省IO时间的可观手段。

所以,减少IO时所要操作的扇区数,就可以提升MySQL的性能。

接着,让我们来看看数据库是如何在扇区中存储数据的, 让我们来举个例子来详细讲解:

如上面两张图,我们创建了一个user(用户)表,一行数据(id属性加上name属性在加上age属性)的大小为128bit,这里我们设定一块扇区的大小是512bit,所以一个扇区可以存储4条用户表的数据;

  • 这时我们来打个比方:我的表中有400条数据,在最坏的情况下查询一条可能就需要查询100个扇区才能找到这个表中你想要的数据,是不是很慢呢?那让我们来加上索引看看:

属性名大小
id6bit
地址10bit

根据图我们可以看到,每一条索引中的指针都记录一个扇区的地址,通过计算机的计算可以得出该索引指向的块中存储着哪些数据。已知索引中的每条数据是16bit(即id大小加上地址大小)。

我们根据上面的例子,一个扇区可以存储16个索引数据,进而得出共需要7个扇区能存下我们的所有索引。这时,我们再来查询一次数据,模拟一下最坏的查询情况,首先遍历索引表,因为最坏的情况下我们需要查询7个扇区就读取到了我们数据表中的数据,然后通过地址找到该数据所在的扇区,即再查找1个扇区,所以总共只需要查询8个扇区就可以查到了我们想要的数据!!!所以是不是快了很多呢!

那有的小伙伴说,既然用了索引可以让搜索变得很快了,那为什么还要采取树的形式呢?

二、数据库采用树作为存储结构的原因

虽然,索引很好的解决了上述问题,大大提高了速度,但仅仅有索引就能解决所有问题么?让我们接着看下面的例子:

假设我们的表数据乘以一百万,那么所有的读取扇区数都要扩大一百万倍,可以看到尽管我们这个表加了索引,速度还是很慢,那有没有什么方法让读取扇区数再次变少呢?

没错,聪明的小伙伴我想已经想到了,再将索引上再加一层索引,如图所示:

尽管数据扩大了一百万倍,但是我们又可以比之前更快的查询到了数据了呢!以此类推,我们根据数据的大小建立索引的层级关系,就可以更快的查询到数据了

现在我们将其图像抽象出来:

然后进行简化再旋转: 我相信只要你细心,我多少能看出来些问题,这个图像好像什么数据结构呢? 其实这就是数据结构中的树!!没错!!这就是为什么要采用树的数据结构进行存储了。

我想现在小伙伴们已经知道了为什么要采用树这种结构了吧。让我们再来向下想,虽然确定了存储数据的基本结构,但是树的结构这么多,如平衡二叉树,红黑树等等,我们具体要采用哪种树呢?

三、B+树的由来

所有成熟理论的形成都是从简到难的过程,MySQL的B+树也不例外。

1,二叉搜索树

在最开始,人们想到的是二叉搜索树(Binary Search Trees)来存储数据库的数据,主要特性是若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值。如图:

但是这个树有个致命的缺点,当有从小到大并且连续的节点数据插入到此树中时,会造成所有节点只有右孩子,导致树变成了链表的形式,那此时查询的时间复杂度为O(n),将会大打折扣。如图:

2,平衡二叉搜索树

为了解决这种办法:我们只能将树进行升级:

这时就出现了另外一种二叉树,平衡二叉搜索树,即AVL Trees (Balanced binary search trees),该二叉树的特点是可以根据平衡因子进行一个自平衡的,如下图所示,我也是依次插入的1到6,此时的数据显然和二叉搜索树不同,是自平衡的。

虽然解决了不平衡问题,但随着互联网的快速发展,百万千万的数据量已经是常事了,那么这时的平衡二叉搜索树就会导致树的度过大,进而会导致磁盘IO的次数增多,达不到想要的效率。

3,B树

很快,聪明的数据库设计人员就解决了这一问题,出现了B树,这种树的特点是每个节点可以存储多个数据了,而且每个节点下面可以存多个子节点,如图可以看到B树在磁盘中的一个存储结构,节点中存储着主键,数据和指针:

这样即使存储的数据再庞大,由于树的度得到了控制,所以查询速度也得到了进一步的加快。

不过这时可能有细心地小伙伴要问了,这和你上面讲的磁盘中的索引结构也不对呀,上边明明是索引和数据分开的呀,到你这怎么索引和数据在一起了呢?

不要着急。B树虽然这样解决了上述的所有问题,那我们的性能能不能再提升一个档次呢?由于内存速度远远高于硬盘速度,能不能把数据一次性读取到内存里边呢,这样我们不就有了更快的性能了吗?

因为内存空间远远小于硬盘空间,而且由于B树的结构,每个节点的既要存储主键即索引,又要存储指针地址,还要把数据存储到里边,所以也只能将部分数据暂时存储在内存里,当我查的数据不在内存,那么我还需要再次进行大量的IO从磁盘上读取数据,显然达不到我们所幻想的把数据存储在内存里边。

不过,这是难不倒我们数据库开发人员的。接下来才是重头戏,B+树出场了!!

4,B+树

B+树在磁盘速度有限的情况下,是比较完美的解决方法了,目前能很好地解决了所有的问题,从图中可以看出B+树将数据都保存在了叶子节点上,并且叶子之间用双向链表的形式连接了起来,也解决了范围查找时频繁遍历树的情况。

所以上面旋转后的索引图其实就是B+树的大致结构,这回是不是解决了小伙伴的疑问了呢,嘿嘿嘿!!

结论

好了!!这就是我所分享的MySQL索引和B+树的所有内容,如果小伙伴对哪里不清楚可以进行留言提问,我会第一时间回复的!!