一、什么是索引
在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构。再直白点就是我们可以把索引理解成图书或者字典的目录。
既然索引是数据的一种存储结构,那么我们必然要对其进行存储,同时,建立索引的目的就是为了加快查询速度,所以必然要选择一种可以高效查询的数据结构来进行存储,而 MySQL 选择了什么数据结构来存储索引呢?
二、二分查找法
在日常开发中,我们经常使用链表或者数组来存储数据,而如果按顺序架构数据存入数组,查找数据时我们就可以可用二分查找法来实现高效查询(注意:因为链表的空间是不连续的,所以不能使用二分查找法)。
二分查找法:Binary Search,也称折半查找法,是一种效率较高的查找方法。比如有 1-10 十个数,现在需要找到 8,先从中间开始找 5,然后发现 8 比 5 大,可以把 5 左边的数排除掉,剩下 6-10,再从中间开始找,依次类推,直到找到 8 为止。
二分查找法有一个前提是数据必须是有序的,存储数据的空间必须是连续的(只有连续的空间才可以通过下标计算出指定数据的位置),这种数据存储一般属于链式存储,我们一但要插入或者修改一个数据,可能会伴随着大量的下标移动,比如我们把 1-10 放在数组里面,下标分别对应 0-9,然后现在要插入一个 0,为了保证有序,0 必须排在第一位,那么 1-10 所有的数据下标都要往后移动一位,这会严重影响到写入性能,所以数组并不适合用来存储索引。
三、二叉树
分查找法是一种效率相对较高的查询方式,但是其有较多的局限性且并不适合于频繁修改的场景,为了解决二分查找的局限和缺陷,有人发明了一种新的数据结构:树。而二叉树又是树中最基本的一种数据结构。
二叉查找树简称二叉树(BST),英文全称:Binary Search Tree,这是一种什么样的数据结构呢?
下图所示就是一棵二叉树:
在上面这个二叉树中,我们要找到 8,先从根节点 6 开始比较,发现 8 比 6 大,继续往右边的子节点查找,这时就可以找到 8。
二叉树有两个特点:
-
左子树所有的节点都小于父节点。
-
右子树所有的节点都大于父节点(这就是上面为什么发现 6 小于当前需要查找节点时需要继续往右边寻找的原因)。
根据上面这棵二叉树,我们可以发现,二叉树的查询效率和这棵树的深度是相关的。因为在实际应用中,每次获取一个节点可能就说明进行了一次磁盘 IO 操作,树越深,需要的 IO 次数可能就会越多,从而查询速度就越慢,在最坏的情况下时间复杂度会退化成 O(n),这时候二叉树就会退化成一个链表,如下图所示:
当二叉树退化成链表之后,这时候的查询效率就会非常低,只能一个个遍历列表中的元素,直到找到需要的节点。
四、平衡二叉树
二叉树退化成链表之后就表明这棵树只有右子节点或者只有左子节点,也就是说左右节点不平衡,那么这时候就有人想办法采用特定算法让二叉树平衡一点,这就是平衡二叉树。
平衡二叉树,英文全名叫做:Balanced binary search trees,简称 AVL 树,这个 AVL 并不是英文名的简称,而是发明者(G. M. Adelson-Velsky 和 E. M. Landis)两个人的人名缩写。
平衡二叉树的特点就是:左右子树深度差绝对值不能超过 1,一旦超过 1 就会通过特定算法来发生左旋或者右旋操作,以此来保证这颗树的平衡性,避免出现普通二叉树的极端情况下退化成为链表的情况。
有了平衡二叉树,似乎是解决了上面提到的问题,那么我们是不是可以选择平衡二叉树来存储索引呢?然而实际上 MySQL 中的索引并不是采用平衡二叉树来进行存储的,这是为什么呢?
索引需要存储哪些信息
在回答这个问题之前,我们需要了解一下,索引到底需要存储什么?
为了达到数据检索效果,一个索引至少应该包含以下三部分的信息:
-
索引值:就是表里面索引列对应的值,因为我们查询就是通过索引值来查询的,所以索引值必然要存储。
-
数据的磁盘地址(通过磁盘地址找到当前数据)或者直接存储整条数据:通过索引搜索的目的其实是需要找到当前对应的整条数据,所以必然需要存储地址或者数据
-
子节点的地址:任何时候查询都是从根节点开始的,当发现根节点的数据并不是我们想要的数据,我们需要继续往下查询,所以需要知道当前根节点中所有子节点的引用地址。
有了上面的三部分存储信息,我们可以得到下面的一个索引存储简图:
上图中,每个节点中黄色区域就表示索引值,紫红色区域(中间部分)表示当前索引值对应的数据磁盘地址,通过这个地址可以直接获取整条数据,最下面蓝色的左、右存储了子节点的地址。
InnoDB 索引结构
在 InnoDB 存储引擎中,页(Page)是用于管理数据的最小磁盘单位,页的默认大小为 16KB(不同版本会有差异)。而这个页也就是对应了上图中的每一个节点,每查询一次节点就需要进行一次 IO 操作。
那么问题就来了,上图中,AVL 树一个节点上只存了一个关键字(索引值)+ 一个磁盘地址+ 左右节点的引用,这几个信息加起来占用了多大空间,我们可以来算一下:
-
索引关键字:假设是采用 32 位的 uuid 进行存储,那么就是占了 32 个字节。
-
数据磁盘地址:这一块其实在 InnoDB 中采用的是 8 个字节进行存储。
-
子节点的引用地址:假设有 2 个子节点,那么这一块也是占用了 8 * 2 字节。
上面三部分内容加起来总共是 56 个字节,而 16kb 有多少个字节呢?答案是 16384 个字节。
所以如果采用平衡二叉树来存储索引的话,我们一个节点存储的内容是远远小于 16kb,这样看来,一个节点只存储一个关键字,浪费了大量的空间。
而且因为二叉树只有两路,数据量一上来,整颗树就会变得非常深,这也会很影响查询性能,所以我们需要做的就是将一棵“瘦高”的树变成“矮胖”的树。
将一棵“瘦高”的树变成“矮胖”的树最简单的办法就是将路数变多,也就是让一个节点存储更多的关键字。
五、红黑树
与AVL树相比,红黑树并不追求严格的平衡,而是大致的平衡。但红黑树的查询效率会有所下降,这是因为树的平衡性变差,高度更高。但红黑树的删除效率大大提高了,因为红黑树同时引入了颜色,当插入或删除数据时,只需要进行O(1)次数的旋转以及变色就能保证基本的平衡,不需要像AVL树进行O(lgn)次数的旋转。
因此,在实际应用中,AVL树的使用相对较少,而红黑树的使用非常广泛。例如,Java中的TreeMap使用红黑树存储排序键值对;Java8中的HashMap使用链表+红黑树解决哈希冲突问题(当冲突节点较少时,使用链表,当冲突节点较多时,使用红黑树)。
对于数据在内存中的情况(如上述的TreeMap和HashMap),红黑树的表现是非常优异的。但是对于数据在磁盘等辅助存储设备中的情况(如MySQL等数据库),红黑树并不擅长,因为红黑树长得还是太高了。当数据在磁盘中时,磁盘IO会成为最大的性能瓶颈,设计的目标应该是尽量减少IO次数;而树的高度越高,增删改查所需要的IO次数也越多,会严重影响性能。
六、多路平衡树(B 树)
B 树有一个特点就是:分叉数(路数)永远比关键字数多 1。
如下图就是一棵简易的 B 树示意图(白色框表示路数,蓝色框表示索引值)
可以看到这棵树的中间一层,存储了两个关键字,所以其下一层就有了三个子节点,所以关键字存储越多,路数越多,同样深度的树就能存储更多的数据。
多路平衡树解决了二叉树和平衡二叉树的问题,目前看起来性能应该不错,但是 MySQL 依然没有选择 B 树来存储索引,原因我们后面解释,我们先来看看 B 树的升级版本:B+ 树。
七、B+ 树
下图是一棵 B+ 树的存储数据示意图:
对比 B 树,其实 B+ 树有一个非常明显的特点,那就是最后一层的叶子节点会有指向下一个节点的指针,从而形成了一个有序链表。
在 InnoDB 中,B+ 树有以下特点:
-
B+ 树的关键字的数量是跟路数相等的。
-
B+ 树的根节点和枝节点中都不会存储数据,只有叶子节点才存储数据。而搜索到关键字也不会直接返回,也仍然会到最后一层的叶子节点。
-
B+ 树的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指向下一个叶子节点的第一个数据,形成了一个有序链表的结构。
八、为什么要选择 B+ 树
B+ 树本身是由 B 树改进而来的,所以 B 树能解决的问题,B+ 树都能解决,而且 B+ 树相比较 B 树更有以下优势:
-
扫库、扫表能力更强:如果我们要对表进行全表扫描,只需要遍历叶子节点就可以了,不需要遍历整棵 B+ 树,因为其数据只存储在叶子节点。
-
B+ 树的磁盘读写能力相对于 B 树来说更强: B+ 树的根节点和枝节点不保存数据,所以一个节点可以保存更多的关键字,一次磁盘加载(即一次 IO 操作)能获取到相对更多的关键字。
-
天然具备排序能力:叶子节点上有下一个数据区的指针,数据形成了链表。
-
效率稳定:B+ 树永远是在叶子节点拿到数据,所以 IO 次数是稳定的,而 B树运气好根节点就拿到数据,运气不好就要到叶子节点才能拿到数据,所花费的时间会有差异。