Mysql数据库为什么使用B+树作为索引?这篇文章我们不鼓吹B+树的优点,我们从数据结构上来看这个问题。
数据结构大致可以分为两种 —— 线性结构 和 非线性结构。
线性结构包括:数组、链表、哈希表、栈、队列 等等
非线性结构包括:树、图
还有例如 跳表 之类的其他的数据结构,也都是从基础数据结构演化出来的,用来解决指定的场景问题。
我们先把记忆中的 Mysql的索引是使用B+树做的,因为B+树有 xxx 的优点 抹去,没有人在开发的时候就能直接想到完美的解决方案,所以我们也来推导一下索引的数据结构。
索引是用来做什么的?用来加快查询的速度。
索引存储在哪里?存储在硬盘里。
我们都说数据持久化数据持久化,其实就是把内存里的数据转移到硬盘上,这样即便是设备断电了,数据也不会受到影响。但是有利必有弊,数据存储在硬盘上带来的后果就是读取的速度变慢。又是变慢,能变多慢呢?内存是纳秒级的处理速度,硬盘是毫秒级的处理速度,二者相差百万倍,这就是速度的差异。所以我们实际使用索引的时候,会把索引从硬盘中读到内存里,然后通过内存里的索引,从硬盘中找到数据。
但是这样优化了又如何呢,只要需要读硬盘,那就会消耗时间,硬盘IO越多,时间消耗越多。
除此之外,我们使用索引不只是为了能够迅速找到某一个数据,而是能够迅速找到某一个范围区间的数据,能够动态的执行有关数据的操作。
那么在上述的描述下,索引能够使用的数据结构就有这么几个 —— 哈希表、跳表、树。
几天前我们便编写了文章,一起解读了 HashMap 的源码,阅读量还蛮可观,如果不清楚 HashMap 的底层实现可以点击这里查看一下与 HashMap 有关的内容。
哈希表的精确查询时间复杂度是 O(1) ,为什么呢?因为计算目标 key 的 hash 值,然后直接对应到数组的下标,这个过程大大的减少了查询所需要的时间。在产生了 hash 碰撞的时候,也会使用链表和红黑树的方式加快碰撞情况下,查询目标值的速度。
这样的话,我们索引的数据结构完全可以采用哈希表的形式来做,效率非常高。但是为什么不这么做呢?
如果使用哈希表来当作索引的数据结构,在进行范围查询的时候需要全部扫描,这是一笔不菲的代价。
跳表是一个比较陌生的数据结构,其实本质上就是链表,但是又做了进一步的调整。
如图所示,跳表就是在链表的基础上加了 索引层 ,这样就能够实现区间查询的效果。比如我们要查找 key = 5,那就先遍历索引层,遍历到 3 ,然后发现下一个索引是 6 ,那么直接从索引层的 3 往下进入链表,在往后走2步就到 key = 5 了。
如果数据量非常非常大呢?(图方便这里用 excel 绘制,美观上会差一些)
这样是不是就能发现跳表的好处了,用多个索引划分链表,从高级索引定位到更低级的索引,直到定位到链表中。效率看起来也很高。
但是这还是存在一个问题,我读取完三级索引到内存,然后我还要硬盘IO去读取二级索引,然后还要读取一级索引。还是在硬盘的IO上费了太多的操作。跳表的数据越多,索引层越高,读取索引带来的硬盘IO次数越多,性能降低,这又违背了一开始使用索引的理念。
树结构的特性决定了遍历数据本身就支持按区间查询。再加上树是非线性结构的优势相比于线性结构的数组,不必像数组的数据是连续存放的。那么当树结构在插入新数据时就不用像数组插入数据前时,需要将数据所在往后的所有数据节点都得往后挪动的开销。所以树结构更适合插入更新等动态操作的数据结构。
实现索引使用的数据结构看来是要使用树结构了,常用的树都有哪些呢?二叉树、二叉查找树、平衡二叉查找树、红黑树、B树。
二叉树的树结构中定义的是每个节点的可以是0个子节或1个子节点,但是最多不超2个子节点。
基于二叉树,我们不难想到完全二叉树、满二叉树的概念。
完全二叉树是树的所有节点和同深度的满二叉树的的节点位置相同。
满二叉树是一棵二叉树的所有非叶子节点都存在左右子节点,并且所有子节点在同一层级。
二叉查找树可以理解为融合了二分查找的二叉树。二分查找大家都熟悉吧,时间复杂度 O(logN) ,比直接遍历的线性查找快的多,但是需要数组是有序的。
所以说,二叉查找树不同于普通的二叉树,二叉查找树是将小于根节点的元素放在左子树,将大于根节点的元素放在右子树。其实就是从某种含义了实现了二分查找的先决条件 —— 数值有序。
但是二叉树是存在弊端的,如果我们每次都插入一个更小的数或者更大的数,那么二叉树就会在一个方向上无限延长,退化成了链表。那链表的时间复杂度是 O(N),而且又加大了硬盘的IO操作。所以这种结构还是不太行。
上面说一直放更小或者更大的数,让他不断延长,变成一个极端的“很高很瘦”的数,那么用平衡二叉查找树就能解决这个问题。
平衡二叉查找树的关键是 平衡 ,指的是每个节点的左右子树高度差不能超过 1。这样左右子树都能平衡,时间复杂度为 O(logN) 。
红黑树也是平衡二叉查找树的一种。
无论是二叉树还是二叉查找树还是平衡二叉查找树还是红黑树,他最终都存在一个问题 —— 每个节点只能有 2 个子树。这意味着只要数据量足够大,它总会变成一个深度非常大的树。深度越大,硬盘IO次数越多,性能效率越低,这又双叒叕与索引的初衷背道而驰。
新的数据结构的产生肯定是为了解决之前繁琐的问题。在树的深度不断变大的情况下,B树就应运而生了。
B树的出现解决了树高度的问题,从名字上也能看出来,它不叫 B二叉树 而是直接叫 B树 ,因为它摆脱了 二叉 这个概念。它不再限制一个父节点中只能有两个子节点,而是允许拥有 M 个子节点(M > 2)。不仅如此,B树的一个节点可以存储多个元素,相比较于前面的那些二叉树数据结构又将整体的树高度降低了。那么B树实际上就是多叉树。
图中每一个节点叫做 页,是Mysql数据读取的基本单位,也就是上面的磁盘块。其中的 P 是指向子节点的指针。
当数据量足够大的时候,使用平衡二叉查找树则会不断纵向扩展子节点,让整个树变得更高。而B树可以横向扩展子节点,变得更胖,但是树的高度不高,硬盘IO的次数更少。
综上所述,B树已经非常适合用来给Mysql做索引的数据结构了。那么为什么还要去使用B+树呢?实际上B树存在一个缺点,虽然B树实现了区间查找,但是B树的去检查找是基于中序遍历来做的,中序遍历的算法题大家应该都做过,需要来回切换父子节点,切换父子节点在这里就意味着硬盘不断的IO操作,这显然也是不好的。
B+树其实就是B树的升级版。MySQL 中innoDB引擎中的索引底层数据结构采用的正是B+树。
B+树相对于树做了这些方面的改动:B+树中的非叶子节点只作为索引,不存储数据。转而由叶子节点存放整棵树的所有数据。叶子节点之间再构成一个从小到大的有序的链表并互相指向相邻的叶子节点,也就是在叶子节点之间形成了有序的双向链表。
画图画的不是很清楚,忘记展示双向链表的特点,最下面的箭头指的是相邻的两个叶子是双向的,所有的叶子节点构成双向链表。
再来看B+树的插入和删除,B+树做了大量冗余节点,从上面可以发现父节点的所有元素都会在子节点中出现,这样当删除一个节点时,可以直接从叶子节点中删除,这样效率更快。
相邻的两个叶子是双向的,所有的叶子节点构成双向链表。
再来看B+树的插入和删除,B+树做了大量冗余节点,从上面可以发现父节点的所有元素都会在子节点中出现,这样当删除一个节点时,可以直接从叶子节点中删除,这样效率更快。
B树相比于B+树,B树没有冗余节点,删除节点时会发生复杂的树变形,而B+树有冗余节点,不会涉及到复杂的树变形。而且B+树的插入也是如此,最多只涉及树的一条分支路径。B+树也不用更多复杂算法,可以类似红黑树的旋转去自动平衡。