一、B+树的基本概念
1. 定义
B + 树是B 树的改良分支 ,专为数据库、文件系统索引设计,是 MySQL、PostgreSQL 等数据库的底层主键索引标准结构。 核心设计目标:大幅优化区间范围查询、全量遍历查询,同时保持极低磁盘 IO 次数。
2. m 阶 B + 树严格约束(和 B 树最大区别)
阶数 m 定义:节点最多存放 m 个子指针,最多 m-1 个索引关键字
- 非叶子节点(索引层)
- 关键字仅做路由索引,不存储完整业务数据;
- 除根节点外,最少
⌈m/2⌉个子节点,最少⌈m/2⌉-1个 key; - 关键字与子指针规则:
k₀ < 子树1 < k₁ < 子树2 < ...,和 B 树逻辑一致。
- 叶子节点(数据层,最核心区别)
- 所有真实完整数据记录只存在叶子节点,非叶子只有索引;
- 叶子节点之间通过双向有序链表串联,按 key 升序排列;
- 叶子节点 key 数量和数据记录一一对应,区间查询只需顺着链表遍历;
- 全局平衡规则:所有叶子节点严格在同一层,树高度完全统一;
- 根节点特殊:树高度 > 1 时,至少 2 个子节点;单节点树无限制。
3. 和标准 B 树最直观差异对照表
| 特性 | B 树 | B + 树 |
|---|---|---|
| 数据存储位置 | 叶子、非叶子节点都存完整数据 | 仅叶子节点存完整数据,上层只有索引 key |
| 叶子节点关系 | 无链表,互相独立 | 双向有序链表串联,全局升序 |
| 等值查询路径 | 可能在非叶子节点命中,提前结束 | 无论查找任何 key,必须走到叶子节点 |
| 范围查询 | 需要多次回溯父节点切换子树,IO 多 | 定位区间起点后,顺着链表遍历,IO 极少 |
| 全表扫描 | 整棵树深度遍历,开销极大 | 直接遍历叶子链表,连续读取磁盘页 |
| 节点填充 | 上下层都存放数据,空间分散 | 上层纯索引,一页能存更多路由,树更低 |
二、B + 树节点结构拆解
1. 非叶子节点(索引页,只用来导航)
bash
[子指针0][key₀][子指针1][key₁][子指针2]...[keyₙ₋₁][子指针n]
- key:真实主键 / 索引字段;
- data:完整行数据(或磁盘行偏移地址);
- 尾部两个指针:
prev上一个叶子、next下一个叶子,形成有序双向链表。
结构示意图(4 阶 B + 树 m=4)

所有叶子节点首尾相连,全局升序。
三、四大核心操作:查找 / 插入 / 删除 / 分裂合并
1. 查找操作(等值查询)
流程
- 从根索引节点逐层匹配分界 key,找到对应下层子树;
- 一直向下遍历,必然抵达叶子节点;
- 在叶子有序数组内二分查找目标 key,匹配则取出 data,无匹配则查找失败。
示例:查找 22
- 根节点
[10,30]→ 22 介于 10、30 之间,进入中间子树[15,22]; - 索引节点
[15,22]→ 22 等于分界 key,进入右侧叶子[20,22]; - 叶子内找到 22,读取对应数据。
范围查询(B + 树碾压 B 树的核心场景)
需求:查询 12 ≤ key ≤ 35
- 向下遍历找到区间起点
12所在叶子; - 顺着叶子节点的
next链表依次读取:[12,15] → [20,22] → [32,35]; - 无需向上回溯、切换分支,连续磁盘顺序读取,性能极高。
2. 插入操作(节点分裂规则)
前置规则
- 插入永远落在叶子节点,上层索引节点仅同步分界 key;
- 叶子插入 key 后,key 总数 ≤ m-1:直接写入,结束;
- 叶子 key 数量达到 m(溢出),执行叶子分裂;
- 分裂后向上更新父索引节点,父节点溢出则递归向上分裂。
4 阶 B + 树分裂完整步骤(m=4,最多 3 个 key)
叶子原有 [20,22,32],插入 35,满 4 个 key 溢出:
- 合并有序数组
[20,22,32,35]; - 从中间切割:左叶子
[20,22],右叶子[32,35]; - 将右叶子第一个 key(32) 提升到父索引节点作为分界值;
- 父节点新增 key=32,若父节点 key 数量达到 m,重复分裂逻辑;
- 若分裂递归到根节点,新建根索引,树高度 + 1。
关键区别 B 树:B 树提升中间 key,B + 树提升分裂后右叶子第一个 key。
3. 删除操作(借值 / 合并逻辑)
删除仅会直接修改叶子节点,上层索引仅做同步更新,分两类场景:
场景 1:删除后叶子 key 数量 ≥ 最低下限 ⌈m/2⌉-1
直接删除当前 key,若删除的 key 是父节点分界值,无需立刻修改上层(索引允许存在已删除的 key,仅作路由)。
场景 2:删除后叶子 key 不足下限(欠载)
方案 A(优先):向左右相邻叶子借关键字 相邻叶子 key 数量充足,取相邻的一个 key 移入当前叶子,同时更新父节点分界 key。
方案 B:相邻叶子无多余 key,执行节点合并 当前叶子 + 相邻叶子合并为一个新叶子,删除父节点对应的分界 key; 若父节点因此欠载,递归向上处理索引层。
特殊:索引层 key 冗余特性
B + 树上层索引的 key 可以在叶子不存在,不影响查询正确性,仅作为路由标记,因此删除叶子数据后不会立刻同步清理上层索引,减少修改开销。
四、B+ 树的基本操作C++代码完整实现
本代码实现标准 m 阶 B + 树,完全采用工业 0 下标(Base-0)开发逻辑,区分索引节点、叶子双向链表,实现插入分裂、删除借键 / 合并、区间查询、双版本中序遍历等全套功能。
(一)结构体与类成员变量
1. struct BPlusNode B + 树节点
cpp
struct BPlusNode {
vector<int> keys; // 关键字数组,有序升序
vector<BPlusNode*> children;// 子节点指针数组,长度=keys.size()+1
bool leaf; // true=叶子节点(存完整数据);false=索引节点(仅分界key)
BPlusNode *prev, *next; // 叶子节点双向链表前驱、后继指针,索引节点无意义
BPlusNode(bool isLeaf = true) : leaf(isLeaf), prev(nullptr), next(nullptr) {}
};
核心区别 B 树:
- 叶子节点额外维护
prev/next双向指针,串联全局有序链表; - 索引节点仅存分界索引,真实数据全部保存在叶子。
2. class BPlusTree 成员变量
cpp
int m; // B+树阶数
int max_keys; // 单节点最大关键字 m-1
int min_keys; // 非根节点最小关键字 ⌈m/2⌉-1 = (m-1)/2
BPlusNode* root; // 根节点
BPlusNode* first_leaf;// 叶子链表头指针,快速遍历全量数据
构造函数 BPlusTree(int order):
cpp
BPlusTree(int order) {
m = order;
max_keys = m - 1;
min_keys = (m - 1) / 2;
root = new BPlusNode(true);
first_leaf = root;
}
- 初始化阶数、节点容量上下限;
- 创建空根(初始为叶子);
first_leaf指向根,作为叶子链表起点。
(二)叶子链表工具函数(B + 树独有核心)
1. BPlusNode* get_leftmost(BPlusNode* n) 获取最左叶子
cpp
BPlusNode* get_leftmost(BPlusNode* n) {
while (!n->leaf) n = n->children[0];
return n;
}
功能:输入任意节点,一路向左遍历到最底层第一个叶子节点
执行流程:
循环判断 !n->leaf,持续取 children[0],直到抵达叶子,返回该叶子指针。
用途:初始化、根分裂后刷新 first_leaf 头指针。
2. void link_leaf(BPlusNode* left, BPlusNode* right) 链接两个相邻叶子
cpp
void link_leaf(BPlusNode* left, BPlusNode* right) {
right->prev = left;
right->next = left->next;
if (left->next) left->next->prev = right;
left->next = right;
}
场景:叶子分裂时,新建右叶子需要插入双向链表
执行流程:
- 右叶子前驱 = 原左节点;
- 右叶子后继 = 原左节点原本的后继;
- 如果左节点原有后继,修改其后继的前驱指向新右节点;
- 左节点后继指向新右节点; 完成双向链表插入。
3. void unlink_leaf(BPlusNode* node) 从链表移除叶子
cpp
void unlink_leaf(BPlusNode* node) {
if (node->prev) node->prev->next = node->next;
if (node->next) node->next->prev = node->prev;
if (node == first_leaf) first_leaf = node->next;
node->prev = node->next = nullptr;
}
场景:叶子节点合并时,删除其中一个叶子,断开链表链接
执行流程:
- 若存在前驱,前驱后继指向 node 的后继;
- 若存在后继,后继前驱指向 node 的前驱;
- 如果当前节点是链表头
first_leaf,更新头指针为下一个叶子; - 清空 node 的 prev/next 指针,脱离链表。
(三)查询相关函数
1. bool search(int key) 等值查找
cpp
bool search(int key) {
BPlusNode* cur = root;
while (!cur->leaf) {
int i = 0;
while ((size_t)i < cur->keys.size() && key >= cur->keys[i]) {
i++;
}
cur = cur->children[i];
}
auto it = lower_bound(cur->keys.begin(), cur->keys.end(), key);
return (it != cur->keys.end() && *it == key);
}
B + 树特性:无论 key 是否匹配索引,必须走到叶子节点才能判断是否存在
执行流程:
cur = root,循环向下遍历索引层:- 遍历当前节点 keys,找到第一个≥key 的分界下标
i; - 进入
children[i]子节点;
- 遍历当前节点 keys,找到第一个≥key 的分界下标
- 抵达叶子节点后,
lower_bound二分查找 key; - 匹配成功返回 true,否则 false。
2. get_min() / get_max() 全局最值
cpp
int get_min() {
return first_leaf->keys[0];
}
int get_max() {
BPlusNode* p = first_leaf;
while (p->next) p = p->next;
return p->keys.back();
}
get_min():直接取first_leaf第一个 key,全局最小值;get_max():从链表头一路向后遍历到最后一个叶子,取末尾 key。
3. vector<int> range_query(int low, int high) 区间查询(B + 树优势)
cpp
vector<int> range_query(int low, int high) {
vector<int> res;
BPlusNode* cur = root;
while (!cur->leaf) {
int i = 0;
while ((size_t)i < cur->keys.size() && low >= cur->keys[i]) {
i++;
}
cur = cur->children[i];
}
while (cur) {
for (int k : cur->keys) {
if (k > high) return res;
if (k >= low) res.push_back(k);
}
cur = cur->next;
}
return res;
}
利用叶子有序链表,无需递归回溯子树,顺序遍历即可
执行流程:
- 向下遍历索引层,找到区间下限
low所在的起始叶子; - 循环遍历当前叶子所有 key:
- key 在
[low,high]存入结果; - key>high 直接终止遍历返回;
- key 在
- 切换到
cur->next下一个叶子,重复遍历。
(四)插入核心:split_child 节点分裂(B + 树分裂规则和 B 树不同)
cpp
void split_child(BPlusNode* parent, int idx) {
BPlusNode* node = parent->children[idx];
BPlusNode* right = new BPlusNode(node->leaf);
int mid;
int up_key;
if (node->leaf) {
mid = (node->keys.size() + 1) / 2;
right->keys.assign(node->keys.begin() + mid, node->keys.end());
node->keys.resize(mid);
up_key = right->keys[0];
link_leaf(node, right);
} else {
mid = node->keys.size() / 2;
up_key = node->keys[mid];
right->keys.assign(node->keys.begin() + mid + 1, node->keys.end());
node->keys.resize(mid);
right->children.assign(node->children.begin() + mid + 1, node->children.end());
node->children.resize(mid + 1);
}
parent->keys.insert(parent->keys.begin() + idx, up_key);
parent->children.insert(parent->children.begin() + idx + 1, right);
}
函数区分两种节点分裂逻辑:叶子节点 / 索引节点
分支 1:叶子节点分裂
mid = (node->keys.size() + 1) / 20 下标整数除法;- 原节点保留前 mid 个 key,新右叶子复制后半段 key;
- 提升的 key 是右叶子第一个 key(B 树提升中间 key,这是核心区别);
- 调用
link_leaf把新右叶子插入双向链表; - 父节点插入提升的分界 key,新增右子节点指针。
分支 2:索引节点分裂(和标准 B 树逻辑一致)
mid = node->keys.size() / 2,取中间 key 上移;- 原节点保留前 mid 个 key 与 mid+1 个子指针;
- 右索引节点复制后半段 key 与子指针;
- 父节点插入中间分界 key,新增右子节点。
2. void insert(int key) 完整插入流程
cpp
void insert(int key) {
BPlusNode* cur = root;
vector<pair<BPlusNode*, int>> stack;
// 向下寻叶匹配规则
while (!cur->leaf) {
int i = 0;
while ((size_t)i < cur->keys.size() && key >= cur->keys[i]) {
i++;
}
stack.emplace_back(cur, i);
cur = cur->children[i];
}
auto it = lower_bound(cur->keys.begin(), cur->keys.end(), key);
if (it != cur->keys.end() && *it == key) {
cout << "重复键\n";
return;
}
cur->keys.insert(it, key);
while ((int)cur->keys.size() > max_keys) {
if (stack.empty()) {
BPlusNode* new_root = new BPlusNode(false);
new_root->children.push_back(cur);
split_child(new_root, 0);
root = new_root;
first_leaf = get_leftmost(root);
break;
}
pair<BPlusNode*, int> item = stack.back();
stack.pop_back();
BPlusNode* p = item.first;
int idx = item.second;
split_child(p, idx);
cur = p;
}
first_leaf = get_leftmost(root);
}
- 栈
stack记录向下遍历路径(父节点 + 子树下标),用于向上回溯分裂; - 向下遍历索引层,找到目标叶子节点;
- 叶子有序插入 key,重复 key 直接提示并退出;
- 循环判断当前节点是否溢出(key 数量 > max_keys):
- 栈为空:根节点溢出,新建上层索引根,执行分裂,更新全局 root;
- 栈不为空:取出父节点与下标,调用
split_child分裂,切换当前节点为父节点继续校验溢出;
- 插入完成后刷新
first_leaf链表头。
(五)删除全套函数(叶子 / 索引分开借键、合并)
叶子节点专用操作
cpp
void borrow_left_leaf(BPlusNode* parent, int idx) {
BPlusNode* cur = parent->children[idx];
BPlusNode* sib = parent->children[idx - 1];
int val = sib->keys.back();
sib->keys.pop_back();
cur->keys.insert(cur->keys.begin(), val);
parent->keys[idx - 1] = cur->keys[0];
}
void borrow_right_leaf(BPlusNode* parent, int idx) {
BPlusNode* cur = parent->children[idx];
BPlusNode* sib = parent->children[idx + 1];
int val = sib->keys[0];
sib->keys.erase(sib->keys.begin());
cur->keys.push_back(val);
parent->keys[idx] = sib->keys[0];
}
void merge_leaf(BPlusNode* parent, int idx, bool left_side) {
BPlusNode* cur = parent->children[idx];
if (left_side) {
BPlusNode* sib = parent->children[idx - 1];
sib->keys.insert(sib->keys.end(), cur->keys.begin(), cur->keys.end());
unlink_leaf(cur);
parent->keys.erase(parent->keys.begin() + idx - 1);
parent->children.erase(parent->children.begin() + idx);
} else {
BPlusNode* sib = parent->children[idx + 1];
cur->keys.insert(cur->keys.end(), sib->keys.begin(), sib->keys.end());
unlink_leaf(sib);
parent->keys.erase(parent->keys.begin() + idx);
parent->children.erase(parent->children.begin() + idx + 1);
}
}
borrow_left_leaf:当前叶子向左邻叶子借 key- 左叶子末尾 key 下沉到当前叶子头部;
- 更新父节点分界 key 为左叶子新末尾值。
borrow_right_leaf:当前叶子向右邻叶子借 key- 右叶子首 key 下沉到当前叶子尾部;
- 更新父节点分界 key 为右叶子新首值。
merge_leaf:两个叶子节点合并,删除其中一个叶子并断开双向链表。
索引节点专用操作
cpp
void borrow_left_idx(BPlusNode* parent, int idx) {
BPlusNode* cur = parent->children[idx];
BPlusNode* sib = parent->children[idx - 1];
int sep = parent->keys[idx - 1];
cur->keys.insert(cur->keys.begin(), sep);
parent->keys[idx - 1] = sib->keys.back();
sib->keys.pop_back();
if (!cur->leaf) {
cur->children.insert(cur->children.begin(), sib->children.back());
sib->children.pop_back();
}
}
void borrow_right_idx(BPlusNode* parent, int idx) {
BPlusNode* cur = parent->children[idx];
BPlusNode* sib = parent->children[idx + 1];
int sep = parent->keys[idx];
cur->keys.push_back(sep);
parent->keys[idx] = sib->keys[0];
sib->keys.erase(sib->keys.begin());
if (!cur->leaf) {
cur->children.push_back(sib->children[0]);
sib->children.erase(sib->children.begin());
}
}
void merge_idx(BPlusNode* parent, int idx, bool left_side) {
BPlusNode* cur = parent->children[idx];
if (left_side) {
BPlusNode* sib = parent->children[idx - 1];
sib->keys.push_back(parent->keys[idx - 1]);
sib->keys.insert(sib->keys.end(), cur->keys.begin(), cur->keys.end());
sib->children.insert(sib->children.end(), cur->children.begin(), cur->children.end());
parent->keys.erase(parent->keys.begin() + idx - 1);
parent->children.erase(parent->children.begin() + idx);
} else {
BPlusNode* sib = parent->children[idx + 1];
cur->keys.push_back(parent->keys[idx]);
cur->keys.insert(cur->keys.end(), sib->keys.begin(), sib->keys.end());
cur->children.insert(cur->children.end(), sib->children.begin(), sib->children.end());
parent->keys.erase(parent->keys.begin() + idx);
parent->children.erase(parent->children.begin() + idx + 1);
}
}
borrow_left_idx:索引节点向左兄弟借分界 key + 子树;borrow_right_idx:索引节点向右兄弟借分界 key + 子树;merge_idx:两个索引节点合并,吸收父节点分隔 key 与全部子树。
fix_underflow 欠载修复主函数
cpp
void fix_underflow(BPlusNode* node, vector<pair<BPlusNode*, int>>& stack) {
while (node != root && (int)node->keys.size() < min_keys) {
pair<BPlusNode*, int> item = stack.back();
stack.pop_back();
BPlusNode* p = item.first;
int idx = item.second;
BPlusNode* left_sib = (idx > 0) ? p->children[idx - 1] : nullptr;
BPlusNode* right_sib = ((size_t)(idx + 1) < p->children.size()) ? p->children[idx + 1] : nullptr;
if (node->leaf) {
if (left_sib && left_sib->leaf && (int)left_sib->keys.size() > min_keys) {
borrow_left_leaf(p, idx);
break;
} else if (right_sib && right_sib->leaf && (int)right_sib->keys.size() > min_keys) {
borrow_right_leaf(p, idx);
break;
} else if (left_sib && left_sib->leaf) {
merge_leaf(p, idx, true);
} else if (right_sib && right_sib->leaf) {
merge_leaf(p, idx, false);
}
} else {
if (left_sib && (int)left_sib->keys.size() > min_keys) {
borrow_left_idx(p, idx);
break;
} else if (right_sib && (int)right_sib->keys.size() > min_keys) {
borrow_right_idx(p, idx);
break;
} else if (left_sib) {
merge_idx(p, idx, true);
} else {
merge_idx(p, idx, false);
}
}
node = p;
}
if (root->keys.empty() && !root->children.empty()) {
root = root->children[0];
root->prev = root->next = nullptr;
first_leaf = get_leftmost(root);
}
}
删除后节点 key 数量低于min_keys时自下而上修复,区分叶子 / 索引两套逻辑:
- 循环条件:当前节点不是根、关键字不足下限;
- 取出父节点与当前子树下标,判断左右兄弟:
- 叶子节点:优先向左右叶子借 key;无富余则合并叶子;
- 索引节点:优先向左右索引兄弟借 key;无富余则合并索引节点;
- 修复完成跳出循环,否则将父节点设为当前节点,继续向上修复;
- 特殊处理根节点:根无 key 但存在子节点,将第一个子节点设为新根,刷新链表头。
void del(int key) 删除主流程
cpp
void del(int key) {
vector<pair<BPlusNode*, int>> stack;
BPlusNode* cur = root;
while (!cur->leaf) {
int i = 0;
while ((size_t)i < cur->keys.size() && key >= cur->keys[i]) {
i++;
}
stack.emplace_back(cur, i);
cur = cur->children[i];
}
auto it = lower_bound(cur->keys.begin(), cur->keys.end(), key);
if (it == cur->keys.end() || *it != key) {
cout << "不存在\n";
return;
}
cur->keys.erase(it);
fix_underflow(cur, stack);
first_leaf = get_leftmost(root);
}
- 栈记录遍历路径,向下找到 key 所在叶子;
- 叶子内删除目标 key;
- 调用
fix_underflow自下而上修复节点欠载、借键、合并; - 刷新叶子链表头
first_leaf。
(六)四大遍历函数(两套中序,区分教学 / 业务场景)
1. traversal_in_business() 业务中序(真实数据,数据库场景)
cpp
vector<int> traversal_in_business() {
vector<int> res;
BPlusNode* p = first_leaf;
while (p) {
res.insert(res.end(), p->keys.begin(), p->keys.end());
p = p->next;
}
return res;
}
B + 树独有,直接遍历叶子双向链表,输出全部真实数据,无索引冗余 key:
cpp
BPlusNode* p = first_leaf;
while (p) {
res.insert(res.end(), p->keys.begin(), p->keys.end());
p = p->next;
}
对应 MySQL 全表扫描、范围查询的底层遍历逻辑。
2. traversal_in_teaching() DFS 教学中序(包含索引分界 key)
cpp
void inorder_dfs(BPlusNode* n, vector<int>& res) {
if (!n) return;
if (n->leaf) {
res.insert(res.end(), n->keys.begin(), n->keys.end());
return;
}
for (int i = 0; (size_t)i < n->keys.size(); i++) {
inorder_dfs(n->children[i], res);
res.push_back(n->keys[i]);
}
inorder_dfs(n->children.back(), res);
}
vector<int> traversal_in_teaching() {
vector<int> r;
inorder_dfs(root, r);
return r;
}
递归深度优先遍历整棵树,索引节点 key 也会输出,仅用于课堂画图、算法演示,不对应真实数据库业务。
3. traversal_pre() 前序遍历
cpp
void preorder(BPlusNode* n, vector<int>& res) {
if (!n) return;
res.insert(res.end(), n->keys.begin(), n->keys.end());
for (auto c : n->children) preorder(c, res);
}
vector<int> traversal_pre() {
vector<int> r;
preorder(root, r);
return r;
}
先输出当前节点全部 key,再依次递归所有子节点。
4. traversal_post() 后序遍历
cpp
void postorder(BPlusNode* n, vector<int>& res) {
if (!n) return;
for (auto c : n->children) postorder(c, res);
res.insert(res.end(), n->keys.begin(), n->keys.end());
}
vector<int> traversal_post() {
vector<int> r;
postorder(root, r);
return r;
}
先递归全部子节点,再输出当前节点全部 key。
5. traversal_level() 层序遍历
cpp
vector<int> traversal_level() {
vector<int> r;
queue<BPlusNode*> q;
q.push(root);
while (!q.empty()) {
auto u = q.front();
q.pop();
r.insert(r.end(), u->keys.begin(), u->keys.end());
for (auto c : u->children) q.push(c);
}
return r;
}
队列广度优先,从上到下、每层从左到右输出节点关键字,用于直观打印树分层结构。
6. range_query 区间遍历
cpp
vector<int> range_query(int low, int high) {
vector<int> res;
BPlusNode* cur = root;
while (!cur->leaf) {
int i = 0;
while ((size_t)i < cur->keys.size() && low >= cur->keys[i]) {
i++;
}
cur = cur->children[i];
}
while (cur) {
for (int k : cur->keys) {
if (k > high) return res;
if (k >= low) res.push_back(k);
}
cur = cur->next;
}
return res;
}
利用叶子链表连续读取,是 B + 树相比 B 树性能碾压的核心函数。
(七)C++完整代码参考地址与运行结果完整展示
1.参考地址
GitHub:https://github.com/hongyuxu0/B-Tree-Visualizer/blob/main/bplustree.cpp
2.运行结果完整展示
四、B + 树为什么是数据库标准索引?四大核心优势
1. 范围查询、分页、全表扫描性能碾压 B 树
叶子双向有序链表,区间查询只需顺序遍历链表,磁盘连续 IO;B 树需要频繁切换子树,随机 IO 多。
2. 树高度更低,磁盘 IO 更少
非叶子节点只存索引 key,一页磁盘可以存放更多分界值,同等数据量下树高度远小于 B 树;百万级数据仅 2~3 次磁盘读取。
3. 数据集中存储,缓存友好
所有真实数据集中在最下层叶子,数据库可缓存上层全部索引页,绝大多数等值查询只需要 1 次叶子磁盘 IO。
4. 增删改维护成本更低
上层索引存在冗余 key,删除叶子数据不需要立刻同步修改上层索引,减少多层节点更新操作。
五、完整实操示例:4 阶 Base-0 B+ 树 插入全过程
初始空树,依次插入:5,15,25,35,45,10,8
步骤 1:初始空树,依次插入 5、15、25
初始根为叶子节点,连续插入 3 个 key,未达到上限 3,无分裂
叶子节点:[5, 15, 25]
链表:[5,15,25](唯一叶子,prev/next=nullptr)
树结构:

步骤 2:插入 35,叶子溢出,执行叶子分裂
原叶子 key 数组:[5,15,25,35],size=4
1.叶子分裂计算 mid=4/2=2(0 下标)
- 左叶子保留下标 0、1:
[5, 15] - 右叶子存放下标 2、3:
[25, 35]
2.提升右叶子第一个 key 25 到父索引;
3.双向链表链接:左叶子 next 指向右叶子,右叶子 prev 指向左叶子;
4.新建非叶子根节点,存放分界 key [25],两个子指针分别指向左、右叶子。
当前完整树结构

叶子链表顺序:[5,15] <-> [25,35]
步骤 3:插入 45,写入右侧叶子
右叶子当前 [25,35],插入 45 后变为 [25,35,45],刚好 3 个 key,未溢出,无需分裂。 树结构不变,仅修改右叶子:

叶子链表:[5,15] <-> [25,35,45]
步骤 4:插入 10,写入左侧叶子
左叶子 [5,15] 插入 10,有序重排为 [5, 10, 15],刚好 3 个 key,无溢出。

叶子链表:[5,10,15] <-> [25,35,45]
步骤 5:插入 8,左侧叶子溢出,叶子分裂
左叶子原有 [5,10,15],插入 8 后数组变为 [5, 8, 10, 15],size=4,触发叶子分裂
1.叶子分裂 mid=4/2=2
- 左子叶子:下标 0、1 →
[5, 8] - 右子叶子:下标 2、3 →
[10, 15]
2.提升右子叶子首 key 10,插入上层父索引节点;
3.链表更新:原左叶子断开,新两个叶子串联,整体链表:[5,8] <-> [10,15] <-> [25,35,45]
4.父索引原有 [25],插入分界 key 10,索引变为 [10, 25],新增中间子指针指向 [10,15]。
最终完整 4 阶 B + 树结构(全部数字插入完成)

叶子链表完整串联:5,8 ↔ 10,15 ↔ 25,35,45
六、高频面试核心考点
- MySQL 为什么不用 B 树,选择 B + 树? ① 范围查询、分页、全表遍历效率极高;② 树高度更低,IO 更少;③ 索引层体积小,内存缓存友好;④ 增删维护开销更低。
- B + 树查找一定走到叶子吗? 是,上层只有索引分界,真实数据仅存叶子,哪怕 key 在索引层匹配,也要下探叶子取数据。
- B + 树叶子链表作用? 支持高效区间查询、分页、统计 count、全表扫描。
- B + 树删除时上层索引 key 不删除会不会出错? 不会,索引仅做路由标记,即使 key 对应数据已删除,只会导向空叶子,查询逻辑不受影响。
- 复合索引(联合主键)底层依旧是 B + 树,叶子 key 存储多字段组合有序值。
七、总结
B+树是一种优化的多路平衡查找树,专为数据库索引设计。其核心特点包括:所有数据仅存储在叶子节点,非叶子节点仅包含索引;叶子节点通过双向链表连接,支持高效范围查询;通过分裂和合并操作保持平衡。相比B树,B+树在范围查询、全表扫描和磁盘IO效率上具有显著优势,因此成为MySQL等数据库的标准索引结构。本文详细解析了B+树的概念、结构、操作原理及C++实现,并对比了其与B树的性能差异,突出了B+树在数据库应用中的四大核心优势。
