MySQL 的 B+ 树 是 InnoDB 存储引擎实现索引的核心数据结构。查询数据的过程涉及从根节点逐层向下查找,最终在叶子节点定位目标数据。以下是基于 聚簇索引(主键索引) 和 二级索引(辅助索引) 的详细查询流程:
一、B+ 树结构回顾
- 多路平衡搜索树:所有叶子节点在同一层,树高极低(通常 3-4 层)。
- 非叶子节点:仅存储键值和子节点指针,不存储实际数据。
- 叶子节点:
- 聚簇索引:直接存储完整数据行(按主键排序)。
- 二级索引:存储索引列值和对应的主键值。
- 叶子节点链表:所有叶子节点通过双向链表连接,支持范围查询。
- 页目录(Slot):每个页内记录按槽分组,通过二分查找快速定位。
二、查询流程详解
1. 聚簇索引(主键索引)查询
以 SELECT * FROM table WHERE id = 100;
为例(id
是主键):
- 定位根节点
- InnoDB 通过数据字典直接获取聚簇索引的根节点位置。
- 若根节点不在缓冲池(Buffer Pool),触发磁盘 I/O 加载到内存。
- 逐层向下查找非叶子节点
- 二分查找:在当前节点的键值中用二分法确定子节点指针(例如根节点键为
(50, 150)
,100
落在50-150
区间,选择对应子节点)。 - 预读优化:InnoDB 可能通过
innodb_read_ahead_threshold
预加载相邻页,减少 I/O 延迟。 - 锁机制:使用共享锁(S 锁)保护页面结构,确保并发安全。
- 二分查找:在当前节点的键值中用二分法确定子节点指针(例如根节点键为
- 定位叶子节点
- 到达叶子节点后,再次通过二分查找确定目标记录的位置(如
id = 100
)。 - 页目录作用:叶子节点内的记录按槽(Slot)分组,每个槽指向该组的最大键值。通过二分查找槽,再遍历链表定位具体记录(例如槽 2 对应
id = 4
,但实际需从槽 1 的id = 2
向下遍历链表找到id = 3
)。
- 到达叶子节点后,再次通过二分查找确定目标记录的位置(如
- 返回数据
- 聚簇索引的叶子节点直接存储完整数据行(如
id=100
的整行数据),解析后返回结果。
- 聚簇索引的叶子节点直接存储完整数据行(如
2. 二级索引(辅助索引)查询
以 SELECT * FROM table WHERE name = 'test';
为例(name
是二级索引):
- 定位二级索引的根节点
- 类似聚簇索引流程,找到二级索引的根节点。
- 逐层查找二级索引的叶子节点
- 通过二分查找在二级索引树中定位到
name='test'
对应的叶子节点。 - 叶子节点内容:存储的是
name
的值和对应的主键值(如id=200
)。
- 通过二分查找在二级索引树中定位到
- 回表查询聚簇索引
- 使用
id=200
作为主键,重复聚簇索引的查询流程(定位根节点 → 逐层查找 → 叶子节点获取完整数据)。 - 覆盖索引优化:如果查询字段全部包含在二级索引中(如
SELECT id FROM table WHERE name = 'test';
),则无需回表,直接返回二级索引的主键值。
- 使用
三、关键机制与优化
- 二分查找
- 每个节点的键值按顺序排列,通过二分法快速缩小查找范围(如槽分组的查找)。
- 页目录与槽
- 每个页内记录按槽分组,槽指向组内最大键值。例如,5 个槽时,查找
id=3
需通过二分定位到槽 2(id=4
),再从槽 1 的id=2
向下遍历链表。
- 每个页内记录按槽分组,槽指向组内最大键值。例如,5 个槽时,查找
- 缓冲池(Buffer Pool)
- 热点数据缓存在内存中,减少磁盘 I/O。冷数据通过 LRU 算法淘汰。
- 预读(Read-Ahead)
- InnoDB 通过顺序预读(
linear read-ahead
)或随机预读(random read-ahead
)提前加载可能访问的页。
- InnoDB 通过顺序预读(
- 锁机制
- 使用闩锁(Latch)和共享锁(S 锁)保护节点操作的原子性,避免并发冲突。
四、B+ 树与 B 树的对比
特性 | B+ 树 | B 树 |
---|---|---|
数据存储位置 | 只在叶子节点 | 所有节点均可存储数据 |
树高 | 更低(非叶子节点存储更多键值) | 更高(节点存储数据导致键值更少) |
范围查询 | 通过叶子链表高效支持 | 需回溯父节点,效率低 |
磁盘 I/O | 更少(树高更低 + 预读优化) | 更多(树高更高 + 随机 I/O) |
五、示例代码模拟
class BPlusTreeNode:
def __init__(self, is_leaf=False):
self.keys = [] # 存储键值
self.children = [] # 子节点或数据引用
self.is_leaf = is_leaf # 是否为叶子节点
class BPlusTree:
def __init__(self, t):
self.root = BPlusTreeNode(True) # 根节点为叶子节点
self.t = t # 最小度数
def insert(self, key, value):
root = self.root
if len(root.keys) == (2 * self.t) - 1: # 根节点满时分裂
new_root = BPlusTreeNode(False)
new_root.children.append(root)
self.split_child(new_root, 0)
self.insert_non_full(new_root, key, value)
else:
self.insert_non_full(root, key, value)
def insert_non_full(self, node, key, value):
i = len(node.keys) - 1
if node.is_leaf: # 插入到叶子节点
node.keys.append(None)
while i >= 0 and key < node.keys[i]:
node.keys[i + 1] = node.keys[i]
i -= 1
node.keys[i + 1] = key
node.children.append(value)
else: # 插入到非叶子节点
while i >= 0 and key < node.keys[i]:
i -= 1
i += 1
if len(node.children[i].keys) == (2 * self.t) - 1:
self.split_child(node, i)
if key > node.keys[i]:
i += 1
self.insert_non_full(node.children[i], key, value)
def split_child(self, parent, index):
# 分裂子节点逻辑(简化)
pass
六、总结
- 聚簇索引:直接通过主键定位叶子节点,返回完整数据,无需回表。
- 二级索引:先通过辅助索引查找主键,再通过主键回表到聚簇索引。
- 性能优化:B+ 树通过低树高、范围查询链表、页目录分组等设计,显著减少磁盘 I/O,提升查询效率。
- 适用场景:B+ 树是数据库索引的黄金结构,尤其适合大规模数据的等值查询和范围扫描。
THE END