索引类似字典目录,通过目录可以快速高效的找到想要的内容,总得来说是一种通过空间换时间提高查询效率的方案。它常包含以下几种实现:
- Hash 类型
- B 树类型
- B+ 树类型
首先对于 Hash 类型来说,它只适合单 key 查询,不能做范围查询(不能对索引值排序),一般很少使用
平衡二叉树具有极高的查询效率,每次判断,只需向左或者向右即可,时间复杂度 log2n。然而由于每个节点只能保存一条数据,对于数据库这种海量数据来说,树的高度太高导致磁盘 I/O 的次数过多,再加上为了维持树的平衡所需的左旋、右旋操作,整体效率很差,这也是为什么索引不使用平衡二叉树或者红黑树的主要原因
B 树实际就是二叉树的进一步变种,它的每个节点可以保存多条数据,并且包含多个指向子节点的引用,维护有序数据并允许顺序访问、插入和删除,比较契合磁盘预读逻辑,更适合读取、写入相对较大的数据块,非常适用于数据库和文件系统
B- 树实际就是 B 树,这块是因为翻译问题
B 树是一种平衡多叉树,对于 m 阶的 B 树,它必须满足以下条件:
- 每个节点最多包含 m 个子节点
- 非根节点至少有 m / 2 个元素
- 如果根节点不是叶子节点,则至少包含两个子节点
- 具有 k 个子节点则包含 k - 1 个元素
- 所有叶子节点必须处于同一水平
对于插入操作,每次在根节点对应位置插入,之后根据以下规则进行调整:
- 若该节点元素个数小于 m - 1,直接插入
- 若该节点元素个数等于 m - 1,引起节点分裂,取中间元素插入到父节点中
- 重复步骤二,直到所有节点符合 B 树的规则,最坏的情况一直分裂到根节点,生成新的根节点,高度增加 1
下面以 3 阶 B 树为例,分别介绍上面三种情况:
- 场景1:直接添加,无须任何处理
- 场景2:分裂,中间节点分裂为父节点
- 场景3:多次分裂,产生新的父节点
注意,上面只是为了画图方便,实际场景中 B 树是这样的:
一个节点如果包含 k 个元素,就包含 k + 1 个子节点引用,上面把这块省略掉了
对于删除操作,首先在 B 树中查找需删除的元素,如果存在就删除,删除后根据以下规则进行调整:
- 被删除元素位于叶子节点且删除后节点中元素数目小于 m / 2,则需要看其相邻兄弟节点是否丰满:如果丰满,则向父节点借一个元素来满足条件(丰满节点提交一个到父节点)、如果都不丰满,父节点下放一个元素,并和其子节点合并为一个新节点,合并完成后父节点也要判断是否需要继续合并
- 如果被删除元素位于非叶子节点,则将它的下一个节点上移,最终相当于删除某个根节点元素,删除会执行上述规则
下面同样以 5 阶 B 树为例,分别演示几种情况:
- 场景一:正常删除,无影响
- 场景二:被删除元素在叶子节点,相邻节点丰满
- 场景三:被删除元素在非叶子节点,子节点丰满
- 场景四:被删除元素在叶子节点,兄弟节点不丰满
最后来看一个 B 树的示例图:
B+ 树是在 B树的基础上,做的更进一步的优化,它的特征如下:
- 有 m 个子树的中间节点包含有 m 个元素,每个元素不保存数据,只用来判断
- 叶子节点中包含了全部元素信息,及指向具体数据的指针,且叶子节点依元素自小而大的顺序链接
看一个 B+ 树的示意图:
MySQL 为什么使用 B+ 树而不是 B 树主要由预读策略导致:计算机存储分为两种:内存和外存,外存就是常说的磁盘
内存操作速度极快,外存磁盘数据基于机械运动。每次磁盘读取数据的时间分为寻道时间、旋转延迟和传输时间。其中主要花费时间再寻道和旋转延时上,传输时间可忽略不及。一般每次磁盘 IO 需要 9ms 左右时间,而每秒差不多能执行上亿次内存操作,也就是说:执行一次 I/O 的时间可以执行几十万条内存操作
考虑到磁盘 I/O 非常慢,操作系统提出局部预读来提高效率,也就是说每当读取某个地址的数据时,其相邻的数据很快也会被访问到,所以磁盘以页为单位读取数据,每次读取一页大小,一页大约 4K 或者 8K
而索引存储在磁盘中,想要更快更高效的查到数据,主要需减少的就是 IO 的次数,IO 次数越少,查询速度越高。B 树的每个节点的元素可以视为一次 I/O 读取,树的高度表示最多的 I/O 次数,因此树的高度越低,查询效率越高。
因此 B+ 树相比 B树 具有以下优点:
- 树更低:B+ 树的非叶子节点没有存储指向元素的具体指针,所需内存更小,树的高度更低
- 效率稳定:B+ 树每次查询总是从根到叶子节点,效率稳定,B 树查找的节点层数低时效率高,层数高时效率低
- 更适合范围查询:B+ 树叶子节点间存在指针,范围查找实际就是指针的遍历
至于红黑树就更不用说了,每个节点都包含两个子节点,树高度极高,查询效率肯定比不上 B+ 树和 B 树