索引是一种用于快速查询和检索数据的数据结构,其本质可以看成一种排序号的数据结构。
索引的作用相当于书的目录。打个比方:在查字典的时候,如果没有目录,那我们就只能一页一页地去查,速度很慢。如果有目录,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
索引底层数据结构存在很多种类型,常见的索引结构有:B树、B+树、Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam ,都使用了 B+树 作为索引结构。
优点:
缺点:
哈希表是键值对的集合,通过 键 即可快速取出对应的 值,因此哈希表可以快速地检索数据(接近O(1))。
# 构造散列函数
def hash(a):
return (a % 8) ^ 7
类似的hash算法如上,通过异或或者移位的方式使得计算结果的规律性不那么明显,增加 hash 算法的可靠性
但是,哈希算法有一个 hash冲突 的问题,也就是说多个不同的 key(a) 最后得到的 index 可能相同。通常情况下,解决办法是 链地址法 ,即将哈希冲突的数据存放在链表中。
HashMap 就是一种常见的 采用 hash 表存储数据的结构(下图为jdk1.8之前的版本,jdk1.8之后的版本在此基础上加入了 红黑树 来解决链表过长带来的查询缓慢问题)
既然 哈希表 这么快,为什么 MySQL 没有使用其作为索引的数据结构呢?
主要是因为 Hash 索引是无序的,不支持顺序和范围查询。假如我们需要对表中的数据进行排序或者范围查询的时候,Hash 索引就不可行了。并且每次 IO 只能取一个。
SELECT * FROM tb1 WHERE id < 500;
在这种范围查询中,直接遍历比500小的叶子节点就够了。而使用 Hash 索引是根据 hash 算法来定位的,不能确保 存储的顺序。
二叉查找树 是一种基于二叉树的数据结构,有以下特点:
当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而 ,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也称为斜树),导致查询效率急剧下降,时间复杂度退化为 O(N)
二叉查找树的性能非常依赖于它的平衡度,这就导致其不适合作为 MySQL 底层索引的数据结构。
AVL树的特点是保证任何节点的左右子树高度之差不超过 1 ,因此也被称为高度平衡二叉树,它的查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。
AVL树采用了旋转操作来保持平衡。主要有四种旋转操作:LL旋转、RR旋转、LR旋转和RL旋转。其中LL旋转和RR旋转分别用于处理左左和右右失衡,而LR旋转 和 RL旋转 用于处理左右和右左失衡。
由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了查询的性能。并且,在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。磁盘IO是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。
除此之外,随着数据的增多,树的高度也会变高,这就意味着磁盘 IO 操作次数增多,会影响整体数据查询的效率。
比如,上图的平衡二叉树的高度为 5 ,那么在访问最底部的节点时,就需要 5 次 磁盘 IO 操作。根本原因是因为它们都是二叉树,也就是每个节点只能保存两个子节点
AVL 树虽然能保持查询操作的时间复杂度在 O(logn),但是因为它本质上是一个二叉树,每个节点只能存储 2 个子节点,那么当节点个数越多的时候,树的高度也会相应变高,这样就会增加磁盘的 IO次数,从而影响数据查询的效率。
为了解决降低树的高度的问题,就引出了 B 树,它不再限制一个节点只能有 2 个 子节点,而是允许 M 个子节点,从而降低 树的高度。
假设 M = 3 ,那么就是一颗 3 阶的 B 树,特点就是每个节点最多有 2 个 (M-1个)数据和最多有 3 个(M 个)子节点,超过这些要求的话就会分裂节点:
假设我们在下面这幅 B 树中查找的索引值是 9 的记录
那么步骤可以分为以下几步:
可以看到 ,一颗 3 阶的 B 树在查询叶子节点中的数据时,由于树的高度是 3 ,所以在查询过程中会发生 3 次磁盘 IO 操作。
而同样的数据放在平衡二叉树下,树的高度就会很高,意味着磁盘 IO 操作会更多。所以 B 树在数据查询中比平衡二叉树的效率高。
但是 B 树的每一个节点都包含数据(索引 + 记录) ,而用户的记录数据的大小很有可能远远超做过了索引数据,这就需要花费更多的 磁盘 IO 操作次数来读到 有用的索引数据。
而且在我们查询位于叶子节点上的数据的时候,其父节点以及之前的节点的数据也会加载到磁盘内存,但是这些数据时没有用的,我们只想获取最终节点上的数据,而不是查询过程中遍历过的节点的数据。所以使用 B 树作为索引的话会占用大量内存资源(这些资源本不应该被占用)
另外,如果使用 B 树来做范围查询的话,需要使用中序遍历,这会涉及到多个节点的磁盘 IO 问题一,从而导致整体速度下降。
B+树就是对 B 树做了一个升级,MySQL 中索引的数据结构就是采用的 B+树:
B+树和 B 树差异的点,主要是以下几个点:
查询效率:
B+树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储索引又存储记录的 B 树, B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更 [矮胖] ,查询底层节点的磁盘 IO 次数会更少。
插入和删除效率:
B+ 树有大量的冗余节点,这导致删除一个节点的时候,可以直接从叶子节点中删除,甚至不需要动非叶子节点。
B+ 树的插入操作也是一样,由于有冗余节点,插入可能存在节点的分裂(节点饱和的话)但是最多只涉及树的一条路径。而且 B+树会自动平衡,不需要做更多复杂的操作
范围查询:
由于 B+树所有的叶子节点搜集通过链表进行连接,这种设计可以帮助我们更直接地获取范围内的数据,直接遍历链表即可,而不需要像B树那样,去遍历树。
在 Mysql 中 innoDB 存储引擎采用的就是 B+树作为索引的数据结构,但是在其基础上加了些改进:
二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。
了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。
而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。
B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。
但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有: