为什么MySQL采用B+树作为索引
千里之行,始于足下。
—— 老子
MySQL的数据是持久化的,意味着数据(索引+记录)是保存到磁盘上的,因为这样即使设备断电了,数据也不会丢失。
磁盘是一个慢的离谱的存储设备,内存的访问速度是纳秒级别的(1 ns = 10 -9 s),而磁盘访问的速度是毫秒级别的(1ms=10 -6 s),也就是说读取同样大小的数据,磁盘中读取的速度比从内存中读取的速度要慢上万倍,甚至几十万倍。
磁盘读写的最小单位是扇区,扇区的大小只有 512B大小,操作系统一次会读写多个扇区,所以操作系统的最小读写单位是块(Block)。Linux中的块大小为4KB,也就是一次磁盘I/O操作会直接读写8个扇区。
由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,然后读入到内存,也就是说查询过程中会发生多次磁盘I/O,而磁盘I/O次数越多,所消耗的时间也就越大。
所以,我们希望索引的数据结构能在尽可能少的磁盘的I/O操作中完成查询工作,因为磁盘I/O操作越少,所消耗的时间也就越小。
另外,MySQL是支持范围查找的,所以索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效的执行范围查找。
所以,要设计一个适合MySQL索引的数据结构,至少满足以下要求:
- 能在尽可能少的磁盘的I/O操作中完成查询工作;
- 要能高效地查询某一个记录,也要能高效地执行范围查找;
二分查找树
二分查找树的特点是一个节点的左子树的所有节点都小于这个节点,右子树的所有节点都大于这个节点,这样我们在查询数据时,不需要计算中间节点的位置了,只需将查找的数据与节点的数据进行比较。
二分查找树存在一个极端情况,会导致它变成一个瘸子。
当每次插入的元素都是二分查找树中最大的元素,二分查找树就会退化成了一条链表,查找数据的时间复杂度变成了O(n)。
由于树是存储在磁盘中的,访问每个节点,都对应一次磁盘I/O操作(假设一个节点的大小 小于 操作系统的最小读写单位块的大小),也就是说树的高度就等于每次查询数据时磁盘IO操作的次数,所以树的高度越高,就会影响查询性能。
二分查找树由于存在退化成链表的可能性,会使得查询操作的时间复杂度从O(log n)升为O(n)。
而且会随着插入的元素越多,树的高度也变高,意味着需要磁盘IO操作的次数就越多,这样导致查询性能严重下降,再加上不能范围查询,所以不适合作为数据库的索引结构。
自平衡二叉树
为了解决二分查找树会在极端情况下退化成链表的问题,后来有人提出平衡二分查找树
(AVL树)。
主要是在二分查找树的基础上增加了一些条件约束:每个节点的左子树和右子树的高度差不能超过1。也就是说节点的左子树和右子树仍然为平衡二叉树,这样的查询操作的时间复杂度就会一直维持在O(log n)。
除了平衡二分查找树,还有很多自平衡的二叉树,比如红黑树,它是通过一些约束条件来达到自平衡,不过红黑树的约束条件比较复杂。
不管平衡二叉查找树还是红黑树,都会随着插入的元素增多,而导致树的高度变高,这意味着磁盘I/O操作次数多,会影响整体数据查询的效率
B树
自平衡二叉树虽然能保持查询操作的时间复杂度在O(log n),但是因为它本质上是一个二叉树,每个节点只能有2个子节点,那么当节点个数越多的时候,树的高度也会相应变高,这样就会增加磁盘的I/O次数,从而影响数据查询的效率。
为了解决降低树的高度的问题,后面就出来了B树,它不再限制一个节点就只能有2个子节点,而是允许M个子节点(M > 2),从而降低树的高度。
B树的每一个节点最多可以包括M个子节点,M称为B树的阶,所以B树就是一个多叉树
假设我们在上图一棵 3 阶的B树中要查找的索引值是9的记录那么步骤可以分为以下几步
- 1.与根节点的索引(4,8)进行比较,9大于 8,那么往右边的子节点走;
- 2.然后该子节点的索引为(10,12),因为9小于10,所以会往该节点的左边子节点走;
- 3.走到索引为9的节点,然后我们找到了索引值9 的节点。
可以看到,一棵3阶的B树在查询叶子节点中的数据时,由于树的高度是3,所以在查询
过程中会发生3 次磁盘 1/0 操作。
而如果同样的节点数量在平衡二叉树的场景下,树的高度就会很高,意味着磁盘I/0 操作会更多。所以B 树在数据查询中比平衡二叉树效率要高。
但是B树的每个节点都包含数据(索引+记录),而用户的记录数据的大小很有可能远远超过了索引数据,这就需要花费更多的磁盘 I/0 操作次数来读到「有用的索引数据」。
而且,在我们查询位于底层的某个节点(比如 A 记录)过程中,「非A记录节点」里的记录数据会从磁盘加载到内存,但是这些记录数据是没用的,我们只是想读取这些节点的索引数据来做比较查询,而「非 A记录节点」里的记录数据对我们是没用的,这样不仅增多磁盘I0 操作次数,也占用内存资源。
另外,如果使用B树来做范围查询的话,需要使用中序遍历,这会涉及多个节点的磁盘I0 问题,从而导致整体速度下降。
B+树
B+树就是对B树做了一个升级,MySQL中索引的数据结构就是采用了B+树,B+树结构如下图:

B+树与B树差异的点:
- 叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引;
- 所有索引都会在叶子节点出现,叶子节点之间构成一个有序链表;
- 非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小);
- 非叶子节点中有多少个子节点,就有多少个索引;
通过三个方面,比较下B+树和B树的性能区别:
1.单点查询
B树进行单个索引查询时,最快可以在O(1)的时间代价内就查到,而从平均时间代价来看,会比B+树稍快一些。
但是B树的查询波动会比较大,因为每个节点即存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。
B+树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的B树,B+树的非叶子节点可以存放更多的索引,因此B+树可以比B树更矮胖,查询底层节点的磁盘I/O次数会更少。
2.插入和删除效率
B+树有大量的冗余节点,这样使得删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,这样删除非常快
注意:B+树对于非叶子节点的子节点和索引的个数,定义方式可能会不同,有的是说非叶子节点的子节点个数为M阶,而索引的个数为M-1(这个是维基百科里的定义),但是在前面介绍B树与B+树的差异时,说的是 非叶子节点中有多少个子节点,就有多少个索引,主要是MySQL用到的B+树就是这个特性。
甚至,B+树在删除根节点的时候,由于存在冗余的节点,所以不会发生复杂的树的变形。
B树则不同,B树没有冗余节点,删除节点的时候非常复杂,比如删除根节点中的数据,可能涉及复杂的树的变形。
B+树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。而且B+树会自动平衡,不需要像更多复杂的算法,类似红黑树的旋转操作等。
因此,B+树的插入和删除效率更高
3.范围查询
B树和B+树等值查询原理基本一致,先从根节点查找,然后对比目标数据的范围,最后递归的进入子节点查找。
因为B+树所有叶子节点间还有一个链表进行连接,这种设计对范围查找非常有帮助,比如说我们想知道12月1日和12月12日之间的订单,这个时候可以先查找12月1日所在的叶子节点,然后利用链表向右遍历,直到找到12月12日的节点,这样就不需要从根节点查询了,进一步节省查询需要的时间。
而B树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘I/O操作,范围查询效率不如B+树。
因此,存在大量范围检索的场景,适合使用B+树,比如数据库。而对于大量的单个索引查询的场景,可以考虑B树,比如nosql的MongoDB

浙公网安备 33010602011771号