浅析二叉树、B树、B+树和MySQL索引底层原理

数据库是后端工程师绕不开的核心技术,而索引则是核心中的核心。在日常工作中,我们每天都在和索引打交道,索引问题也是高级工程师面试中,面试官最喜欢考察的地方。

很多开发者在面试中谈及索引时,往往只能给出一些零散、机械的记忆性答案,比如"B+树查询快"、"索引能提速"。这样的回答在面试官心中留下的深刻印象就是此候选人学习知识浅尝辄止,不够深入,不够努力。真正体现一个工程师技术深度的,是对索引背后设计原理的系统性理解,以及在复杂场景下进行选型和优化的能力。

在 MySQL 中,通常所说的索引,如果没有特别声明,默认都是指 B+ 树数据结构的索引。在介绍各种树的数据结构和MySQL索引之前,先介绍本文多次使用的几个基本概念:

节点 :包含一个数据元素及若干指向子树的指针等。
根节点 :是树的起始节点,它没有父节点,就像是大树的根基,其它节点都从根节点衍生出来。
叶子节点 :也叫终端节点,是没有子节点的节点,它们位于树的最底层,就像树枝的末梢。

内部节点 (internal Node)‌:至少有一个子节点的节点,包括根节点和所有非叶子节点。
兄弟节点 : 具有相同父节点的节点。
节点的度 :该节点拥有的子节点个数。例如二叉树中节点的度最大为 2,即每个节点最多有两个子节点。
树的度 :整棵树中所有节点的最大度数。
层级 :从根节点开始,树的每一层都是一个层级。
高度 (height):当前节点到最远叶子节点的最长路径上的节点数。
平衡因子 (Balance Factor):简称BF,每个节点的左右子树的高度之差。
数据页 :树上每个节点在计算机中叫做数据页,是 InnoDB 存储引擎与磁盘交互的最小单位,默认大小为 16KB。
阶数:一个节点最多可以有多少个子节点(即节点的最大度数),一般用小写字母m表示阶数。用⌈m/2⌉表示对m/2向上取整数。例如,三阶 B+ 树 → 每个节点最多 3 个子节点 → 最大度 = 3。

在数据库中我们将B+树作为索引结构,可以加快查询速度,此时树中的key一般是表的主键。例如,MySQL InnoDB 的记录,就是按照主键由小到大排序后,存在 B+ 树的叶子节点里。树中每个节点存储了关键字(key)、关键字对应的数据(data)以及指向孩子节点的指针。我们将一个key及其对应的data称为一条 记录。但为了方便描述,除非特别说明,后续文中就用"关键字"来代指(key, value)键值对。下面就是MySQL中索引的数据结构:

图1 索引的数据结构。备注:磁盘块4有笔误,应该删除10。

不同的树结构适用于不同的应用场景,选择合适的树结构可以显著提高程序的性能和效率。在分析为什么MySQL InnoDB存储引擎的索引选择使用B+树之前,我相信很多攻城狮对数据结构中的树还是有些许模糊的,因此就让我们一同踏上这趟由浅入深探讨二叉查找树、AVL树、红黑树、B树和B*树的奇妙之旅,一步步引出B+树以及为什么MySQL InnoDB数据库索引选择使用B+树!领略B+树在数据库索引中应用的神奇魅力。希望通过本文,你能构建起一个体系化的树和索引知识框架,让你在未来的面试和工作中游刃有余。

二叉查找树

二叉查找树(Binary Search Tree, BST)也称为有序二叉树(Ordered Binary Tree)、排序二叉树(sorted binary tree)或者二叉搜索树,是一棵满足以下三个性质的二叉树:

  1. 有序性:任意非空左子树所有节点的值均小于该节点的值;非空右子树所有节点的值均大于于该节点的值;
  2. 递归性:任意节点的子树都是二叉查找树;
  3. 每个节点存储一条记录,且没有键值相等的节点。

从递归定义的角度来看,二叉查找树可以被定义为:要么是一棵空树,要么是由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;而左子树和右子树又同样都是二叉查找树。这种递归定义方式简洁而准确地描述了二叉查找树的结构特点。它作为基础的树形结构,每个节点最多有两个子节点,其递归性和有序性为我们理解更复杂的树结构奠定了基础。

图2 二叉搜索树

如果我们需要查找id=31的记录,利用我们创建的BST,基于二分查找算法实现的查找流程如下:

  1. 将根节点作为当前节点,把31与当前节点的键值33比较,31小于33,接下来我们把当前节点的左子节点作为当前节点。
  2. 继续把31和当前节点的键值28比较,发现31大于28,把当前节点的右子节点作为当前节点。
  3. 把31和当前节点的键值31对比,发现二者相等,满足条件,遍历结束。

所以,我们利用BST只需要3次即可找到目标数据。

二叉树的查找、插入和删除操作的时间复杂度在平均情况下为 O(logn) ,但在最坏情况下,当二叉树退化为链表时,时间复杂度会变为 O(n) 。这是因为在最坏情况下,所有节点都在一条链上,查找、插入和删除都需要遍历整个链表。如下图所示退化的二叉查找树:

二叉树完全不平衡时,查找的时间复杂度就和链表一致了:O(n)。导致这个现象的原因其实是BST变得不平衡了,也就是高度太高了,从而导致查找效率的不稳定。为了解决这个问题,我们需要保证二叉查找树一直保持平衡,从而引出了一个新的定义------平衡二叉树AVL。

AVL树/红黑树

平衡二叉树(Balanced Binary Tree)是一种特殊的、平衡的二叉搜索树,通过严格控制每个节点的平衡因子来保持树的平衡。常见的平衡二叉树有AVL树和红黑树等,在满足BST特性的基础上,具有如下特性:

  1. 平衡因子限制‌:要求平衡因子的绝对值不能超过1。
  2. 递归性质‌:左右两个子树本身也都是平衡二叉树。

不管是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转树上的节点来保持平衡,使得树的高度始终保持在 O(logn)的范围内。而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入或者删除次数比较少,但查找多的情况。

🤔 为什么要平衡?

保持效率:平衡的二叉搜索树可以确保不管是执行插入还是删除操作都能在O(logn)时间内完成,这对于需要高效处理数据的系统来说至关重要。

避免最坏情况:没有平衡机制的数据结构在最坏情况下可能会退化成链表,导致效率大大降低。平衡机制通过旋转操作

完成,而旋转操作非常耗时,由此我们可以知道AVL树适合用于插入或者删除次数比较少,但查找多的情况。本文不介绍具体的旋转操作。

平衡二叉树通过约束节点的子树高度差,确保树的高度保持对数级,相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。

红黑树(Red-Black Tree)因节点是红色和黑色两种颜色之一而命名,它本身是一棵二叉查找树,在其基础上做了如下约束:

  • 根节点是黑色节点
  • 所有叶子节点(NIL节点)都是黑色的
  • 节点的颜色要么是黑色要么是红色
  • 无连续红色节点:红色节点的子节点为黑色节点(即相邻的两个节点不可能同时为红色)
  • 任意节点的左子树和右子树高度(只统计黑色节点)相同
  • 旋转操作:当插入或删除导致树不平衡时,通过旋转操作来恢复平衡。

这些规则看起来是否有点复杂?其实它们的核心目的只有一个:控制整棵树的高度,防止退化成链表。

下图是一个标准的红黑树:

黑色完美平衡:"任意节点的左子树和右子树高度(只统计黑色节点)相同 & 红色节点的子节点是黑色节点"。这就保证了红黑树的"平衡性"。由于这种平衡,其查询的复杂度自然也是O(log n)的。

红黑树与AVL树的区别:

特性 AVL 树 红黑树
平衡性 非常严格,平衡因子最多为1 近似平衡,只要求从根到叶子的最长路径不超过最短路径的两倍
插入性能 多次旋转,效率较低 快(最多两次旋转)
删除性能 多次旋转,效率更低,代价更高 较快(最多三次旋转)
查找性能 更快 稍慢(但仍为 O(log n))

相较于 AVL 树,它是一种弱平衡二叉树,不需要像 AVL 树那样严格要求平衡,这也导致它的查询效率并没有 AVL 树那么高,但相对的,它带来的好处是对于调整失衡,红黑树的旋转次数更少,所以频繁的插入或删除操作,红黑树更有优势。

AVL 是"强迫症患者",每次插入删除都可能引发多次旋转,代价太高;而红黑树是"实用主义者",它不要求完全平衡,只要能保证查找效率不崩就行。红黑树是在"速度"、"稳定性"和"实现成本"之间找到的一个折中王者!

红黑树更常用于需要高频插入和删除的场景,例如:java 8 HashMap和ConcurrentHashMap中链表转红黑树;epoll在内核中的实现,用红黑树管理事件块(文件描述符);Java的TreeMap和TreeSet实现;linux进程CFS调度器(Completely Fair Scheduler)用红黑树管理进程控制块 ;nginx中,用红黑树管理timer。AVL树更适合读多写少的场景,因为它的查找性能更接近完美的平衡树。

B树(B-tree)

当数据量太大,无法全部装入内存时,就必须存放在磁盘上。磁盘I/O(读写)速度远远逊色于内存访问。B树和B+树就是为了减少磁盘I/O次数而设计的多路平衡搜索树,它们被广泛用于数据库和文件系统的索引。下面介绍B树。

B树(Balance Tree)也称B-树,是一棵平衡多路查找树,节点的子节点个数可能超过两个,用来存储排序后的数据。我们描述一棵B树时需要指定它的阶数m,当m=2时,就是我们常见的二叉搜索树。B树是一种自平衡的树状数据结构,能够让数据的查找、插入及删除动作都在对数时间内完成,常常用在存储系统上,如数据库或文件系统。

一棵m阶的B树,或为空树,或为满足下列特性的m叉树:

  1. 每个节点最多有m(m>=2)棵子树,除非根节点为叶子节点,否则,至少有两棵子树。例如,对于一个4阶B树,每个节点最多有4个孩子,最少有2个孩子;
  2. 每个节点存放至少 ⌈m/2⌉-1 个关键字,至多 m-1 个关键字;
  3. 关键字个数比孩子节点个数小1。即如果一个节点有k个关键字,那么它就有k + 1个孩子。如下图所示为一棵3阶B树的结构示意图,根节点有2个关键字和3个指针,它有3个孩子,这些孩子分别对应关键字划分的3个区间:
  1. 若根节点不是叶子节点,则至少包含两棵子树(特殊情况:没有孩子的根节点,即根节点为叶子节点,整棵树只有一个根节点);
  2. 除根节点和叶子节点外,其它每个节点至少有 ⌈m/2⌉ 棵子树;
  3. 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它;
  4. 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。这保证了B树的平衡性,使得查找操作的时间复杂度为O(logn)。

下图即是一棵 m=3 的B树:

在实际应用中B树的阶数都非常大,通常大于100,所以即使存储大量的数据,B树的高度仍然比较小。每个节点中存储了关键字(key)和关键字对应的数据(data)以及孩子节点的指针。B树和平衡二叉树的不同之处:B树属于多叉树,父节点的子节点个数不止两个。注意: 有文章把B树和B-tree理解成了两种不同类别的树,其实二者是同一种树。B树有如下数据特征:

  1. 数据存储在整棵树中;
  2. 任何一个关键字出现且只出现在一个节点中;
  3. 搜索有可能在非叶子节点结束,故查询性能不稳定;
  4. 其搜索性能等价于在关键字全集内做一次二分查找;
  5. 自动平衡树的高度和数据页。

B-树的查询方式如下:从根节点开始,对节点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复,直到所对应的儿子指针为空,或已经是叶子节点。

B+树(B+ Tree)

B+树是在B树的基础上又一次改进的多路平衡搜索树,其主要在两个方面进行了提升,一方面是查询的稳定性,让查询速度更加稳定,其速度完全接近于二分查找;另外一方面是在数据排序方面更友好,更充分的利用节点的空间。B+树包含两种类型的节点:内部节点(也称索引节点)和叶子节点。根节点本身即可以是内部节点,也可以是叶子节点。B+树的规则与B树基本类似,但是又在B树的基础上做了以下几点改进:

  • 父节点存有指向右子树第一个元素的索引。
  • 内部节点不存储数据,只存储索引,用于路由,叶子节点存完整行数据(主键索引)或 (索引列 + 主键)(二级索引)。这是B+树与B树的最大差异。
  • 内部节点的子树指针与关键字个数相同。
  • 内部节点的关键字都按照从小到大的顺序排列,对于内部节点中的一个关键字,左子树中的所有关键字都小于它,右子树中的关键字都大于等于它。
  • 所有叶子节点都存储指向下一个相邻叶子节点的指针,形成一个单向链表。

下图是一棵m=3的B+树:

MySQL中的B+树是上述B+树的又一次进化:MySQL基于普通B+Tree,在叶子节点添加了一个指向上一个相邻叶子节点的指针,并且首尾叶子节点也是相连的,也就是构造了一个如图1所示的双链表。下文再提及B+树时,指带有双向链表的MySQL B+树。

B+树相对于B树有一些自己的巨大优势,可以归结为下面几点:

优势 说明
更稳定的查询效率 数据仅存储在叶子节点,任何查找都必须走到叶子节点,路径长度相同,性能稳定。
更高的空间利用率 内部节点不存数据,存储的关键字更多,树更矮,读取数据时I/O次数更少。
强大的范围查询 叶子节点通过指向前后叶子节点的指针形成了一个有序双向链表,故可以高效地进行范围查询和反向排序,而B树需要进行复杂的中序遍历。
更适合磁盘预读 磁盘按数据页读写。B+树的节点通常设计为恰好一数据页大小,一次I/O能加载更多键,进一步减少I/O。

B+树的插入操作很简单,只需要记住一个技巧即可:当节点元素数量大于m-1的时候,就按中间元素分裂成左右两部分,中间元素的关键字分裂到父节点当做索引存储,但是,本身中间元素还是分裂到右子树中。

面试题:B+树为什么比B树更适合作为操作系统的文件索引和数据库索引?

简答如下:
B+树的磁盘读写代价更低。它的内部节点没有存储记录,因此内部节点相对B树占用的存储空间更小。如果把所有同一内部节点的关键字放在同一块磁盘中,盘块所能容纳的关键字数量也就越多,一次性读入内存中的需要查找的关键字也就越多,相对I/O读写次数降低。

B+树的查询效率更加稳定。由于非终节点并不是最终指向文件内容的节点,而只是叶子节点中关键字的索引。所以任何关键字的查找必须走一条从根节点到叶子节点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

B+树支持范围查询。B+树只要遍历叶子节点就可以实现整棵树的遍历,支持基于范围查询,而B树不支持范围查找。

面试题:为什么 InnoDB 选择 B+ 树作为索引结构?

相对于二叉树,B+ 树的层级更少,搜索效率更高。相对 Hash 索引,B+ 树支持范围查找以及排序操作。对于 B- 树,无论是叶子节点还是非叶子节点都会保存数据,这样会导致一页中存储的键值减少,指针跟着减少;要同样保存大量数据,只能增加树的高度,导致性能降低。B+ 树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引。这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+ 树更加 "矮胖",I/O 操作更少。因为卫星数据的不同,导致查询过程也不同;B- 树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所说性能很不稳定,而 B+ 树每次必须查找到叶子结点,性能稳定。

B*树(B star Tree)

B树是普通B+树的变形,在普通B+树除根节点外的内部节点增加指向兄弟节点的指针,将节点的最低利用率从普通B+树的1/2提升到2/3。下图是一棵m=3的B树:

在B+树的基础上因其初始化的容量变大,使得节点空间使用率更高,而又存有兄弟节点的指针,可以向兄弟节点转移关键字的特性使得B*树分裂次数变得更少。

B+树的分裂:当一个节点满时,分配一个新的节点,并将原节点中1/2的数据复制到新节点,最后在父节点中增加新节点的指针;B+树的分裂只影响原节点和父节点,而不会影响兄弟节点,所以它不需要指向兄弟节点的指针。

B树的分裂:当一个节点满时,如果它的下一个兄弟节点未满,那么将一部分数据移到兄弟节点中,再在原节点插入关键字,最后修改父节点中兄弟节点的关键字(因为兄弟节点的关键字范围改变了);如果兄弟也满了,则在原节点与兄弟节点之间增加新节点,并各贡献三分之一的数据到新节点,最后在父节点增加新节点的指针。所以,B树分配新节点的概率比B+树要低,空间使用率更高。

树的总结

1、相同思想和策略

从平衡二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分查找和数据平衡策略来提升查找数据的速度。

2、不同的方式对树的不断优化

为了保证树的节点均匀分布,所以在二叉树的基础上加上了平衡算法,就有了平衡二叉树。

为了减少树的高度,所以B树一个节点下面可以添加N个子节点,然后每个节点的大小默认限制在16KB,只需要通过一次IO就能读取到节点上的所有数据,通过增加节点存储的数据减少了树的高度,而并没有让IO次数变多。

B+树在B树的基础上,对查询的稳定性和排序策略进行了优化,因为B+树所有的数据都保存到叶子节点,并且所有叶子节点本身是有序排列的。

B*树为了减少树在构建过程中节点的分裂或者合并次数,所以在每个非根节点的内部节点上都保存了兄弟节点的指针,在节点需要进行分裂或者合并时,优先从兄弟节点挪数据,从而减少构建过程中节点分裂或者合并的次数,提升了树的构建性能,将节点的最低利用率从1/2提高到2/3。

实际应用中的索引优化

了解 B树 和 B+树的基本原理后,在实际应用中如何合理使用索引呢?首先,要选择合适的索引列。对于经常用作查询条件和排序的列,应该建立索引。其次,要注意索引的维护。频繁更新的列会导致索引重建,因此要权衡索引的利弊。最后,要根据查询需求选择合适的索引类型。对于需要频繁进行范围查询的列,B+ 树索引是更好的选择。

在实际应用中,还可以通过复合索引、覆盖索引等高级技术进一步优化查询性能。

聚集索引和辅助索引

MySQL InnoDB存储引擎数据库中的B+Tree索引可以分为聚集索引(clustered index,也叫主键索引、聚簇索引)和辅助索引(secondary index,也叫非聚集索引)。

聚集索引也叫主键索引,叶子节点中的data存储的是该主键对应的整行数据,通常B+Tree的高度为3,也就是有三层节点,MySQL会把B+Tree第一层也就是根节点放在内存中,我们根据主键索引查数据,只需要两次磁盘I\O(第二层1次,第三层1次)即可。

非聚集索引也叫辅助索引,叶子节点中存储的是该索引所在记录的主键值,所以非聚集索引的寻址过程分两种情况:

(1) 非聚集索引已经索引覆盖了,那么只需要遍历这非聚集索引这一个B+Tree即可,按照上面的分析,需要两次磁盘IO即可(mysql会把根节点放到内存中)。

(2) 非聚集索引不能索引覆盖,那么需要回表。先需要在非聚集索引这个B+Tree上两次IO找到主键,然后拿着主键去聚集索引的B+Tree上找对应的完整数据记录。相比第一种情况IO次数要多,所以我们通常喜欢索引覆盖。这个过程会增加额外的 IO 消耗和网络传输时间,降低查询性能。

下图表示非聚集索引不能索引覆盖的情况:右侧的辅助索引先拿到主键值5,然后去左侧的主键索引中寻址,最后可以得到整行记录的内容。

面试实战指南

掌握了以上各种树的知识,你已经能应对80%的索引问题。但想在面试中真正征服面试官,还需要准备一些更具深度和广度的话题。这里再考察索引问题的时候,有经验的面试官很可能对以下三个问题深挖,你可以准确回答了吗?

  • B+树与B树、红黑树等其它树的对比
  • MySQL索引会失效吗
  • MySQL InnoDB表中含有NULL值的列建索引有作用吗
  • MySQL InnoDB高度为3的B+Tree可以存储多少条数据

答案即将揭晓,敬请关注小编。

结束语

本文介绍了二叉树、AVL树、红黑树、B树和B+树五种树的数据结构,介绍了如何一步步降低树的高度,提升增删改查效率,最后介绍了MySQL InnoDB索引及其相关高频、高阶面试题。

请暂放工作喧嚣,享受汗水换来的闲暇,惬意阅读。聆听内心,感受自由,静享丰盈时光。愿这份宁静和这篇博文,滋养你前行。

相关推荐
文艺理科生2 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling2 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅2 小时前
springBoot项目有几个端口
java·spring boot·后端
Luke君607972 小时前
Spring Flux方法总结
后端
define95272 小时前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li2 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶3 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_3 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路3 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway