首先抛个问题"如何进行sql优化?"
讲到sql优化很多人第一反应:"不就是加个索引嘛?!",的确,加索引确实可以提高sql的查询效率,但为什么在编写sql进行查询的时候还是会出现索引失效导致全表扫描的情况?其实,根本原因在于你没有理解索引的底层设计------B+树数据结构。
什么是B+树?
1、普通二叉树
了解B+树之前,我们先了解一下最简单的二叉树

可以看出二叉树每个节点最多有两个子节点的树结构,子节点分为左孩子和右孩子。它本身只是一种树形结构,不保证有序。
因为不保证插入值的有序性所以查找某个值必须遍历整棵树(O(n)),后面为了保证值有序 的结构来实现高效查找就有了二叉搜索树
2、二叉搜索树(BST):
如下图所示:

每个节点最多有两个子节点的树结构,子节点分为左孩子和右孩子。
-
对于任意节点,左子树所有节点的值 < 该节点的值
-
右子树所有节点的值 > 该节点的值
中序遍历 (左→根→右)会得到一个升序序列:30、40、45、50、55、60、99
BST 通过值有序实现了快速查找,但普通BST在极端情况下会退化成链表(如顺序插入 1,2,3,4,5),查找又变成 O(n)。

于是又出现了平衡二叉树(AVL、红黑树),在保持值有序的同时,通过旋转维持树的高度平衡,保证 O(log n) 的操作效率
3、 平衡二叉树
-
AVL树:严格平衡,左右子树高度差不超过1,查找快,但插入删除旋转多。
-
红黑树:近似平衡,插入删除效率高,广泛用于内存数据结构(如Java TreeMap)。



如果左右子树超过1,那么就会通过旋转保证树的平衡,如图在插入88时正常情况下是插入到70这个节点的右边但是会导致66节点的左右子树高度超过1,所以会旋转一下节点66和节点88作为70的子节点
主要是看某个子树的左边和右边的子节点有没有大于1,如果超过了1,比如66这个节点的子树,左边是0个子节点,右边是2个子节点>1所以就需要找个平衡节点70进行左旋转
问题:每个节点最多两个子节点,树的高度较高。当数据量很大时,树会变得很高,导致磁盘I/O次数多。对于数据库这种以磁盘为存储介质的场景,二叉树不适合,所以就有了下面的B树数据结构。
4、B树(B-Tree)
B树是一种多路平衡查找树,每个节点可以拥有多于两个的子节点。它被设计用于磁盘等外存储设备,降低树的高度,减少磁盘I/O。


如上图所示,每个节点最多有 3个子节点,所以每个节点最多有 2 个关键字
结构特点
-
每个节点最多有 m 个子节点(m称为阶)。
-
每个节点最多有 m-1 个关键字。
-
根节点至少有2个子节点(除非是叶子)。
-
非根非叶节点至少有 ⌈m/2⌉ 个子节点,即至少 ⌈m/2⌉-1 个关键字。
-
所有叶子节点都在同一层,树是完全平衡的。
-
每个节点都存储数据(关键字和对应的数据记录或指针)。
树高度低,磁盘I/O少。节点大小通常设计为一个磁盘页(如16KB),一次I/O读取一个节点,效率高。但是每个节点都存储数据(关键字/索引和值),导致节点能容纳的关键字数量减少,树可能略高。范围查询需要中序遍历,跨层跳转,效率一般。
5、B+树(B+ Tree)
B+树是B树的变体,专门为数据库和文件系统优化。核心区别:非叶子节点只存储键(索引),不存储数据;所有数据都存储在叶子节点,且叶子节点之间用链表连接。


结构特点
-
非叶子节点仅包含键值和指向子节点的指针,不包含数据记录。
-
所有数据记录都存储在叶子节点。
-
叶子节点包含所有键值及对应的数据(或数据指针)。
-
叶子节点之间通过双向链表连接,支持高效范围查询。
-
所有叶子节点在同一层,树完全平衡。
-
每个节点(页)能容纳更多的键,因为非叶子节点不存数据,所以树更矮。
| 对比项 | 二叉树(BST/平衡树) | B树 | B+树 |
|---|---|---|---|
| 子节点数 | 最多2个 | 最多m个 | 最多m个 |
| 数据存储位置 | 每个节点都存数据 | 每个节点都存数据 | 仅叶子节点存数据,非叶子只存键 |
| 叶子节点链接 | 无 | 无 | 双向链表连接 |
| 树的高度 | 较高(log₂n) | 较低(logₘn) | 更低(非叶子不存数据,可存更多键) |
| 查找路径 | 可能提前命中 | 可能提前命中 | 必须到叶子 |
| 范围查询 | 中序遍历,效率低 | 中序遍历,跨层跳转 | 叶子链表顺序扫描,效率极高 |
| 磁盘I/O | 高(节点小,树高) | 较低 | 最低(树矮,节点大) |
| 适用场景 | 内存数据结构 | 文件系统、部分数据库 | 数据库索引(MySQL InnoDB)、文件系统 |
| 平衡性 | 需额外平衡(AVL/红黑) | 自平衡 | 自平衡 |
总结:为什么选择B+树
-
减少磁盘I/O:非叶子节点不存数据,每个页能容纳更多键,树更矮。通常3~4层就能存储上亿条记录,查找只需3~4次I/O。
-
范围查询高效 :叶子节点链表使得
BETWEEN、>、<、ORDER BY等操作可以顺序扫描。 -
全表扫描快:只需遍历叶子链表,无需遍历整棵树。
-
查询性能稳定:每次查找都走到叶子,路径长度相同,性能可预测。
-
更适合磁盘预读:节点大小与磁盘页对齐,一次I/O读取一个节点,预读相邻页。