红黑树、AVL 树、B 树、B+ 树简介
如果面试官比较认真,常常会让你现场推导插入/删除。
可能的形式
- 给你一组数字,手动画出红黑树插入过程
- 插入某个节点后,哪些情况需要变色?哪些情况需要旋转?
- 删除红黑树某个黑节点后怎么修复?
- 画一个跳表,插入 8、12、15、19 后长什么样
- 给一个 B+ 树阶数,让你模拟插入导致分裂
- 删除 B+ 树某个 key,看看是否触发借位和合并
3. 红黑树 vs AVL 树
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在插入和删除操作后能够通过旋转和重新着色来保持树的平衡。
红黑树的特点如下:(根叶黑;不红红;黑路同;)
- 根节点和叶子节点(NULL节点)都是黑色的。
- 每个节点都有一个颜色,红色或黑色。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 从根节点到叶子节点或空子节点的每条路径上,黑色节点的数量是相同的。
红黑树通过这些特性来保持树的平衡,确保最长路径不超过最短路径的两倍,从而保证了在最坏情况下的搜索、插入和删除操作的时间复杂度都为 O(logN)。

查询性能的对比:
- AVL 树:AVL 树是严格的平衡二叉搜索树,它要求每个节点的左右子树的高度差(平衡因子)不超过 1。这种严格的平衡特性使得 AVL 树的高度始终保持在 O(log n),其中 n 是树中节点的数量。在进行查询操作时,由于树的高度相对较低且较为均匀,所以查找任意节点的时间复杂度稳定为 O(log n)。这意味着在理想情况下,AVL 树的查询效率非常高,能快速定位到目标节点。
- 红黑树:红黑树是一种弱平衡的二叉搜索树,它通过颜色标记和特定的规则(如每个节点要么是红色,要么是黑色;根节点是黑色;每个叶子节点(NULL 节点,空节点)是黑色;如果一个节点是红色的,则它的两个子节点都是黑色的;对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点来维持大致的平衡。红黑树的高度通常比 AVL 树略高,其高度上限为 2log(n + 1),因此查询操作的时间复杂度同样为 (log n),但在实际应用中,由于树的高度相对较高,其查询性能可能会略逊于 AVL 树。
在查询性能上,AVL 树由于其严格的平衡特性,表现会稍好于红黑树,但差距通常不大。
插入性能的对比:
- AVL 树:在插入新节点后,AVL 树可能会破坏原有的平衡结构,需要通过旋转操作(单旋转或双旋转)来重新平衡树。由于 AVL 树对平衡的要求非常严格,插入操作后可能需要进行多次旋转来恢复平衡,特别是在树的高度较高时,插入操作可能会引发较多的旋转操作,导致插入性能受到一定影响。插入操作的平均时间复杂度虽然也是 O(log n),但由于旋转操作的开销,实际插入效率相对较低。
- 红黑树:红黑树在插入新节点后,同样可能会破坏树的平衡,但它只需要进行少量的颜色调整和最多两次旋转操作就能恢复平衡。红黑树的平衡规则相对宽松,使得在插入操作时不需要像 AVL 树那样频繁地进行旋转操作,因此插入性能相对较好。插入操作的平均时间复杂度同样为 O(log n),但由于减少了旋转操作的次数,实际插入效率更高。
在插入性能上,红黑树由于其弱平衡特性,表现优于 AVL 树。
在实际应用中,如果查询操作频繁,对查询性能要求较高,且插入和删除操作相对较少,可以选择 AVL 树;如果插入和删除操作较为频繁,对插入性能有较高要求,同时查询性能也能接受一定的损耗,则红黑树是更好的选择。
3.1 为什么红黑树能保证近似平衡?
因为红黑树通过:
- 不允许连续红节点
- 任意路径黑高相同
限制了树的不平衡程度。
直观理解:
- 每条路径上的黑节点数一样;
- 红节点最多只能夹在黑节点之间,不能连续出现;
所以最长路径长度不会超过最短路径长度的 2 倍,因此它不是严格平衡,但能保持近似平衡。
3.2 红黑树的高度为什么是 O(log n)?
红黑树的高度能严格保证在 \(O(\log n)\),根本原因在于它的五条基本性质相互制约,特别是其中两条性质的结合,直接从结构上限制了树的极度不平衡。
我们可以通过一个相对直观的数学推导来理解这背后的逻辑。
决定高度的两条核心性质
- 无连续红节点: 如果一个节点是红色的,那么它的两个子节点必须是黑色的。这意味着在从根节点到叶子节点的任何一条路径上,绝对不可能出现两个相邻的红色节点。
- 黑高平衡: 从任意节点到其各个叶子节点的所有简单路径上,包含的黑色节点数量必须完全相同。这个固定的黑色节点数量被称为该节点的黑高,记为 \(B=bh(x)\)。
数学推导过程
-
假设有一棵包含 \(n\) 个内部节点的红黑树,根节点的黑高为 \(B\),整棵树的实际高度(即最长路径长度)为 \(h\)。
-
步骤一:实际高度 \(h\) 与黑高 \(B\) 的关系
由于性质 4 的限制(不能有连续的红节点),在最极端的“拉长”情况下,一条路径上的节点颜色只能是红黑交替的(黑-红-黑-红...)。
这意味着,任何一条路径上的红色节点数量,最多等于黑色节点的数量。因此,树的最大总高度 \(h\) 最多是黑高 \(B\) 的两倍:\(h \le 2B\)
-
步骤二:节点总数 \(n\) 与黑高 \(B\) 的关系
通过数学归纳法可以严格证明一个引理:以任意节点 \(x\) 为根的子树,至少包含 \(2^B - 1\) 个内部节点
应用到整棵树的根节点上,也就是节点总数 \(n\) 的下界为:\(n \ge 2^B - 1\)
-
步骤三:推导最终结论
现在我们将上面两个不等式结合起来。
由 \(h \le 2B\) 可以得出 \(B \ge h/2\)。
将其代入第二个公式:\(n \ge 2^{h/2} - 1\)
不等式两边同时加 1,并以 2 为底取对数:\(\log_2(n+1) \ge h/2\)
最终得到:\(h \le 2\log_2(n+1)\)
通过最终的不等式 \(h \le 2\log_2(n+1)\) 可以清晰地看到,无论红黑树怎么变动,它的高度 \(h\) 永远被限制在节点数 \(n\) 的对数级别范围内。这就是为什么它的高度界限被证明为 \(O(\log n)\)。
这种巧妙的规则设计使得红黑树在最坏情况下的查找、插入和删除操作都能保持 \(O(\log n)\) 的时间复杂度。它不像 AVL 树那样要求极度严苛的绝对平衡(AVL 树高度约为 \(1.44 \log_2 n\)),因此在进行插入和删除时引发的结构调整(旋转)更少,整体性能更加均衡。
3.3 红黑树插入和删除操作
插入操作
红黑树的插入处理可以概括为两个核心阶段:标准二叉搜索树插入和插入后的颜色与结构修复。
为了将对树平衡的破坏降到最低,红黑树采用了一个极为聪明的核心策略:新插入的节点默认总是红色。这是因为插入红色节点最多只会引发“连续红节点”的冲突,而如果插入黑色节点,则会立刻破坏“所有路径黑高相同”的绝对铁律,后者的修复难度极大。
插入红色新节点后,我们需要根据其父节点和叔叔节点(父节点的兄弟节点)的状态进行修复。
以下是插入修复的完整分类处理逻辑:
(1)初始简单情况
- 情景 A:树是空的。 新节点成为根节点。根据红黑树规则,直接将它染成黑色即可。
- 情景 B:父节点是黑色。 插入红色子节点没有违反任何红黑树性质,不需要任何额外修复,直接结束。
(2)复杂情况:父节点是红色
- 不存在 uncle 节点
- 存在 uncle 节点
删除操作
红黑树的删除比插入复杂很多,整体分 3 步:
- 找到删除节点
- 找到一个替换节点接替删除节点的位置
- 删除节点有两个非空子节点:在这种情况下,通常会找到该节点的后继或前驱(中序遍历下的下一个节点或上一个节点),这将是该节点右子树中的最小值(或左子树的最大值)。然后,将后继或前驱的值复制到要删除的节点中,并删除后继或前驱节点。因为后继节点(或前驱节点)至多只有一个非空子节点,所以这步骤将问题简化为删除有一个或没有子节点的节点。
- 删除节点最多有一个非空子节点:任意找到一个空子节点,然后将另一个子节点(可能也是空的, 也可能是非空), 替代当前节点。
- 修复红黑树高度
按照二叉搜索树删除节点的方式,找到要删除的节点,假设要被删除的节点是 z。那么根据 z 的节点颜色可以分出下面两种情况
z是红色节点,删除红色节点是比较容易的,因为不会打破红黑树的性质,就是正常的二叉搜索树删除节点的操作,这里不展开z是黑色节点,删除黑色节点可能打破红黑树的性质——从任一节点到其每个叶子节点的路径都包含相同数量的的黑色节点,在红黑树中,遇到这种情况仍然是需要用树的旋转和节点重新上色🎨解决问题。下面的讨论主要考虑的是这种情况
具体的参考 这里,我不想看了。
参考资料
3.4 删除为什么比插入更麻烦?
因为删除尤其是删除黑节点时,可能会破坏:
- 黑高一致性
- 红黑约束
插入通常只会造成“连续红”问题,而删除黑节点会让某条路径少一个黑节点,这就是所谓的“黑色缺失”,修复过程更复杂。
本质原因:
- 插入是“局部多了红”
- 删除是“路径黑高可能少了 1”
后者比前者更难处理。
3.5 左旋和右旋分别在干什么?会不会手写?
树的旋转操作,本质上是在不破坏二叉查找树性质(左节点 < 根节点 < 右节点)的前提下,通过局部改变节点间的指针连接关系,来调整树的局部高度,从而维持整棵树的平衡。
简单来说,就是把“太长”的那一边的节点往上提,“太短”的那一边往下压。
左旋 (Left Rotation)
- 核心动作: 逆时针旋转。把目标节点 \(x\) 向左下压,让它的右孩子 \(y\) “上位”成为新的父节点。
- 节点交接: 因为 \(y\) 上位后,它的左孩子位置空出来了,而 \(x\) 被压到了左下角,原本 \(y\) 的左子树(值介于 \(x\) 和 \(y\) 之间)就顺理成章地变成了 \(x\) 的右子树。
右旋 (Right Rotation)
- 核心动作: 顺时针旋转。把目标节点 \(y\) 向右下压,让它的左孩子 \(x\) “上位”成为新的父节点。
- 节点交接: 因为 \(x\) 上位后,它的右孩子位置空出来了,而 \(y\) 被压到了右下角,原本 \(x\) 的右子树(值介于 \(x\) 和 \(y\) 之间)就变成了 \(y\) 的左子树。
旋转操作考察的纯粹是指针操作的严谨性。每一个旋转都涉及三对指针的断开与重连:子树的交接、父节点的交接、以及根节点的交接。
以下是标准 C++ 的实现逻辑:
// 定义红黑树节点
struct Node {
int data;
Node* left;
Node* right;
Node* parent;
bool isRed; // 颜色标识位
};
class RedBlackTree {
private:
Node* root;
public:
// 左旋 (以节点 x 为支点)
void leftRotate(Node* x) {
if (!x || !x->right) return;
Node* y = x->right; // y 是 x 的右孩子,即将上位
// 1. 交接 y 的左子树给 x 的右边
x->right = y->left;
if (y->left != nullptr) {
y->left->parent = x;
}
// 2. 交接 y 的父节点(y 取代 x 的位置)
y->parent = x->parent;
if (x->parent == nullptr) {
root = y; // x 原本是根节点,现在 y 成了新的根
} else if (x == x->parent->left) {
x->parent->left = y;
} else {
x->parent->right = y;
}
// 3. 把 x 变成 y 的左孩子
y->left = x;
x->parent = y;
}
// 右旋 (以节点 y 为支点)
void rightRotate(Node* y) {
if (!y || !y->left) return;
Node* x = y->left; // x 是 y 的左孩子,即将上位
// 1. 交接 x 的右子树给 y 的左边
y->left = x->right;
if (x->right != nullptr) {
x->right->parent = y;
}
// 2. 交接 x 的父节点(x 取代 y 的位置)
x->parent = y->parent;
if (y->parent == nullptr) {
root = x; // y 原本是根节点,现在 x 成了新的根
} else if (y == y->parent->right) {
y->parent->right = x;
} else {
y->parent->left = x;
}
// 3. 把 y 变成 x 的右孩子
x->right = y;
y->parent = x;
}
};
避坑指南:
- 手写旋转时最容易出错的地方是忘记更新
parent指针,或者在处理根节点时发生空指针异常(Segmentation Fault)。所以在写的时候,每次改变一条向下的指针(比如x->right = ...),都要立刻条件反射地去更新对应节点的向上的指针(...->parent = x)。
3.6 Redis 的 zset 为什么选择跳表而不是红黑树?
(1)范围查询
- 红黑树做范围查询需要中序遍历,比较复杂;而跳表的底层本质是一个双向链表,找到起点后,直接顺着指针遍历即可,极其高效(应对
ZRANGE等命令)。
(2)内存占用的精妙控制
- 很多人直觉上认为跳表需要维护多层级的指针,会比红黑树更占内存。但在 Redis 的精妙实现下,事实恰好相反。
- 红黑树由于需要固定维护两个指针和一个颜色位,在 64 位系统中开销很大;
- 而跳表虽然需要维护多层级的指针,但节点层高的生成是通过抛硬币(随机数)决定的。Redis 并没有使用标准的 \(p=0.5\)(即每往上一层概率减半),而是将概率设为了 \(p=0.25\)。这意味着平均每个节点包含的指针数大约是 \(1 / (1 - 0.25) \approx 1.33\) 个。
- 相比红黑树固定的 2 个指针,Redis 的跳表在内存占用上其实更加紧凑和灵活。同时,可以通过调整这个概率参数,在内存消耗和查询速度之间做定制化平滑折中(Trade-off)。
(3)实现复杂度与调试
- 红黑树的左旋、右旋逻辑复杂,容易写出 Bug,而跳表的逻辑直观得多,代码实现和维护成本低。
(4)并发操作与锁粒度
- 尽管 Redis 是单线程处理命令,但在多线程或并发环境下,修改红黑树可能导致树的结构整体变动,需要锁住较大的范围;
- 而跳表的节点修改通常只影响局部指针,更适合实现细粒度锁或无锁并发(尽管 Redis 没用到这点,但在 JUC 的
ConcurrentSkipListMap中是关键)。
3.7 Linux 内核里哪些地方用过红黑树?
内核里凡是需要 动态有序集合、同时要求查找/插删稳定 \(O(log^n)\) 的地方,红黑树都很合适。
1. 进程调度:完全公平调度器 (CFS - Completely Fair Scheduler)
这是内核中红黑树最著名、也最核心的应用之一。CFS 不使用传统的多级优先队列,而是用红黑树来管理所有处于可运行状态的进程。
- 如何运作:红黑树以进程的虚拟运行时间(
vruntime)作为排序的 Key。内核每次调度时,根本不需要遍历,直接摘取红黑树最左侧的节点(即vruntime最小、最渴望得到 CPU 时间的进程)投入运行即可。 - 优势:进程状态切换(睡眠、唤醒)极其频繁,红黑树保证了调度的插入、删除操作始终稳定在 \(O(\log n)\)。
2. 多路复用:Epoll 核心结构
在服务端高并发网络编程中,epoll 相比 select/poll 实现了性能的飞跃,其底层核心数据结构就是一棵红黑树 + 一个双向链表。
- 如何运作:内核中的
eventpoll对象使用红黑树来存储所有被监控的 socket 文件描述符(FD)。红黑树的 Key 是 FD。 - 优势:当开发者调用
epoll_ctl去添加、修改或删除海量并发连接中的某一个 FD 时,红黑树能提供稳定的极速检索,并防止同一个 FD 被重复添加。
3. 内存管理:虚拟内存区域 (VMA)
每个用户进程的虚拟地址空间会被划分为多个不重叠的、连续的内存区域(vm_area_struct,例如代码段、数据段、堆、栈等),内核长期使用红黑树来管理这些 VMA。
- 如何运作:以内存块的起始地址作为 Key。当发生缺页中断(Page Fault)时,内核必须根据触发异常的内存地址,极快地查找到它属于哪个 VMA,从而决定是分配物理内存还是抛出段错误(Segmentation Fault)。
- 注:随着系统内存越来越大,为了更好地适应现代 CPU 的缓存行(Cache Line),从 Linux 6.1 开始,内核开始逐步用一种新的 B树变种(Maple Tree)来替换 VMA 的红黑树,但红黑树在这里依然统治了长达二十多年。
4. 高精度定时器 (hrtimers)
Linux 需要处理海量的定时任务(如超时重传、睡眠唤醒等),高精度定时器子系统使用红黑树来管理这些定时器。
- 如何运作:以定时器的到期时间(绝对时间)作为 Key。红黑树最左侧的节点就是马上要触发的下一个定时事件。
- 优势:随着时间推移,不断有新定时器加入和旧定时器取消,红黑树的高效变更是保证定时器精度的基础。
4. B 树、B+ 树
跳表在内存中(比如 Redis)如鱼得水,但如果把场景切换到 磁盘存储(比如 MySQL),跳表就不适用了,因为跳表的节点在内存中是分散的,如果放在磁盘上会导致极其随机的物理 I/O。
为了解决 磁盘 I/O 瓶颈,计算机科学家们设计了 B 树系列。我们来深度剖析一下 B Tree 和 B+ Tree,以及 InnoDB 的最终抉择。
4.1 B Tree
很多时候大家会看到“B-Tree”这个词,中间的 连字符 经常被误读为“B减树”,其实它就是 B 树(多路平衡查找树)。不存在 B 减树😀。

如上图所示,A、B、C 这些字母在这里是 “关键字”,每个指针节点是 “孩子”。显然的,我们可以看出,关键字个数比孩子个数少一个,因此,对于 m 阶 B 树:
- 它有 m 个孩子,m-1 个关键字
- 除根节点外,最少有
ceil(m/2)个孩子,ceil(m/2) - 1个关键字 - 根节点特殊:若根不是叶子,至少有 2 个孩子;若整棵树只有一个根,则根可以少于上述下限
1. 核心结构与特征:
- B 树是一种多路(不仅有两个子节点,可以有 \(M\) 个子节点)的自平衡查找树。
- 最致命的特征: 在 B 树中,所有节点(包括根节点、内部节点和叶子节点)都同时存储 Key(索引键值)和 Data(完整的行数据或指向行数据的指针)。
- 任何一个普通的查找操作,只要在沿途的节点中匹配到了 Key,就会直接返回该节点里的 Data,搜索提前结束。
2. 存在的痛点:
虽然它的时间复杂度也是 \(O(\log N)\),但由于内部节点里塞满了臃肿的 Data,导致一个固定大小的磁盘页(Page)能装下的 Key 的数量极其有限。
4.2 B+ Tree
B+ 树是 B 树的一种变体,它是专门为磁盘等外存储设备设计的一种平衡查找树。
1. 核心结构与特征:
- 非叶子节点(内部节点)“减负”: 内部节点绝对不存储 Data,只存储 Key 作为向下寻找的路由向导。
- 数据全在底层: 所有的 Data(真实的完整行数据)全部、且仅仅存储在最底层的叶子节点中。
- 叶子节点手拉手: 所有的叶子节点被一个双向链表串联了起来,形成了一个天然的、按主键严格排序的单链。
4.3 MySQL 为什么坚决选择 B+ 树而不是 B 树?
这是面试中最关键的考点。MySQL 选用 B+ 树,完全是为了向磁盘的物理特性妥协并做到极致优化。核心原因有以下三点:
(1)更少的磁盘 I/O 开销
- 磁盘读取原理: 操作系统读取磁盘不是按字节读的,而是按“页(Page)”读的(InnoDB 默认一页是 16KB)。
- B 树的劣势: 如果用 B 树,因为非叶子节点里存了巨大的行数据,16KB 的一页可能只能存下 10 几条记录。当数据量上千万时,B 树会变得非常“高瘦”,树的高度 \(h\) 可能达到 5 甚至更高。这意味着一次查询需要进行 5 次随机磁盘 I/O,这在数据库层面是不可接受的延迟。
- B+ 树的优势: 因为非叶子节点只存极小的 Key(比如一个
BIGINT占 8 字节,加一个指针 6 字节),16KB 的一页可以轻松塞下 1000 多个 Key。 - 数学奇迹: 一棵高度仅仅为 3 的 B+ 树,就能支撑 \(1000 \times 1000 \times 1000 = 10\) 亿级别的数据量!这意味着在 InnoDB 中,查询上亿级别表里的一条数据,最多只需要 3 次磁盘 I/O(而且通常根节点常驻内存,实际只需要 2 次)。B+ 树极其“矮胖”,完美击中了磁盘 I/O 的痛点。
(2)碾压级的“范围查询 (Range Query)”能力
- 数据库中充斥着大量的范围查询需求:
SELECT * FROM user WHERE age BETWEEN 20 AND 30;。 - 如果在 B 树中: 找到 20 之后,为了找后续的值,必须进行繁琐的树的中序遍历。这需要在不同的磁盘页之间疯狂跳转,产生大量的随机 I/O,磁头会剧烈寻道,性能极差。
- 如果在 B+ 树中: 由于所有数据都在叶子节点,并且叶子节点用双向链表连成了单轨火车。定位到 20 这个节点(时间复杂度 \(O(\log N)\))后,根本不需要再回溯整棵树,直接顺着叶子节点的指针往后遍历链表即可。这是极其高效的磁盘顺序 I/O操作。
(3)查询性能极其稳定(企业级应用的偏好)
- 在 B 树中,如果你运气好,查询的数据正好在根节点,1 次 I/O 就搞定了;如果运气差,数据在叶子节点,可能要 5 次 I/O。查询耗时忽高忽低(抖动)。
- 在 B+ 树中,所有的数据都在叶子节点,没有任何捷径。这意味着所有的查询都必须从根走到叶子,所经历的磁盘 I/O 次数是恒定的。在企业级高并发系统中,稳定、可预期的查询延迟往往比偶尔的“运气好”更为重要。
4.4 为什么数据库索引页大小常设计成和磁盘页/缓存页对齐?
因为数据库读写的基本单位通常是“页”,操作系统和磁盘缓存也以页为单位管理。
对齐后有几个好处:
- 一次 I/O 正好读一个完整节点
- 减少页内碎片和跨页访问
- 更好利用缓存局部性
- 简化页管理和缓冲池实现
所以 B+ 树节点通常设计成“一个节点对应一个页”。
4.5 B+ 树一个节点能存多少 key 取决于什么?
B+ 树一个节点能存多少个 Key,本质上是一个“在固定的空间里能塞下多少个数据单元”的物理内存或磁盘空间分配问题。
在数据库(如 MySQL、PostgreSQL)或底层存储引擎的实现中,这个数量主要取决于以下 4 个核心因素:
1. 节点(页/块)的总大小 (Page/Block Size)
B+ 树的节点通常直接对应操作系统或数据库的一次 I/O 读取单位,称为“页”或“块”。
- 节点越大,能存的 Key 就越多。
- 比如在 MySQL 的 InnoDB 引擎中,默认的页大小是 16KB(16384 字节)。而操作系统默认的内存页通常是 4KB。
2. Key 自身的数据类型大小 (Key Size)
你选择的索引列的数据类型直接决定了 Key 的大小。
- 如果以
INT类型作为主键,一个 Key 占 4 字节。- 如果以
BIGINT类型作为主键,一个 Key 占 8 字节。- 如果用
VARCHAR(36)存 UUID 字符串,那一个 Key 就会占用更多空间。Key 越大,一个节点能存的数量就越少。3. 子节点指针的大小 (Pointer Size)
B+ 树的非叶子节点(内部节点)只起到导航作用,除了存 Key,还要存指向下一层子节点的指针(通常是子节点所在的“页号”或物理偏移量)。
- 在 InnoDB 中,这个指针(称为指向子页的 Page Number)固定占用 6 字节。
4. 节点内部的元数据开销 (Header/Metadata Overhead)
每个节点不能把 100% 的空间全拿来存 Key,它需要维护自身的管理信息。
- 比如页头(Page Header)、页尾检查和(Checksum)、事务 ID 等。在 InnoDB 中,这些杂项大概会占据一两百个字节。
具体能存多少?(以 MySQL InnoDB 为例的计算)
我们可以通过一个简单的公式来估算非叶子节点的 Key 容量:
\[\text{节点容量} \approx \frac{\text{节点总大小} - \text{元数据开销}}{\text{Key 大小} + \text{指针大小}} \]假设我们使用默认的 16KB 页大小,以 BIGINT(8 字节) 为主键:
- 一对
(Key + 指针)的大小是:\(8 + 6 = 14\) 字节。- 忽略掉极小的元数据开销,16KB 大约是 16384 字节。
- 容量计算:\(16384 \div 14 \approx 1170\)
结论:在上述条件下,一个非叶子节点大约能存 1170 个 Key 和 1170 个指针。这就是为什么 B+ 树被称为“矮胖型”数据结构——它的扇出(分支因子)非常大,这使得它能够以极低的层数(通常 3 到 4 层)管理千万级别的数据,从而把缓慢的磁盘 I/O 次数降到最低。
注意:叶子节点的容量计算略有不同
B+ 树的叶子节点不存指针,而是存具体的数据(Payload)。如果是聚簇索引(比如 InnoDB 的主键索引),叶子节点存的是完整的行数据(可能高达几千字节),这时候一个叶子节点可能只能存十几个 Key;如果是非聚簇索引,存的则是主键的值。
4.6 为什么自增主键对 B+ 树更友好,而随机 UUID 可能不好?
因为 B+ 树叶子节点按 key 有序存储。
自增主键:新记录通常插到最右侧,属于顺序追加:
- 页分裂少
- 写入局部性好
- 缓存命中高
- 索引维护成本低
随机 UUID:新记录会随机插入到各个位置:
- 更容易触发页分裂
- 页空间利用率下降
- 写放大更明显
- 缓存局部性差
所以数据库里自增主键通常更“友好”。

浙公网安备 33010602011771号