“MySQL 的每个 B+ 树叶子节点是一行数据?”—— 这个说法不完全准确。
在InnoDB 存储引擎中,B+ 树的叶子节点存储的是完整的行数据(聚簇索引)或主键值(二级索引),但一个叶子节点通常包含多行数据,而非“一行一节点”。
一、B+ 树节点结构:页(Page)是基本单位
▶ 1.InnoDB 页(Page)
- 大小:默认16KB(可配置)
- 类型:
- 数据页(Leaf Page):存储实际数据
- 索引页(Non-Leaf Page):存储指针
▶ 2.叶子节点内容
| 索引类型 | 叶子节点存储内容 |
|---|---|
| 聚簇索引(主键索引) | 完整行数据(包括所有列) |
| 二级索引(辅助索引) | 索引列 + 主键值 |
💡核心认知:
一个叶子节点 = 一个 16KB 页 ≈ 多行数据(非单行)
二、聚簇索引 vs 二级索引
▶ 1.聚簇索引(Clustered Index)
- 结构:
[页1: 行1, 行2, ..., 行N] ↔ [页2: 行N+1, ..., 行M] ↔ ... - 特点:
- 数据即索引:行数据按主键顺序存储在叶子节点
- 无需回表:直接返回完整数据
▶ 2.二级索引(Secondary Index)
- 结构:
[页1: (name='Alice', id=100), (name='Bob', id=200), ...] - 特点:
- 索引列 + 主键:叶子节点不存完整数据
- 需要回表:通过主键到聚簇索引查完整行
📌关键点:
二级索引的叶子节点 ≠ 完整行数据
三、行数据如何填充分页?
▶ 1.行格式(Row Format)
- Compact(默认):
- 每行包含变长字段长度列表 + NULL 标记 + 数据
- Dynamic(推荐):
- 大字段(如 TEXT/BLOB)仅存指针,数据存溢出页
▶ 2.单页容纳行数
- 计算公式:
每页行数 ≈ 16KB / 单行平均大小 - 示例:
- 行大小 1KB → 每页 ≈ 15 行
- 行大小 100B → 每页 ≈ 150 行
▶ 3.分裂与合并
- 插入新行:
- 若页满 →分裂为两个页(5:5 或 9:1)
- 删除行:
- 若页利用率 < 50% →尝试合并相邻页
四、查询如何利用 B+ 树?
▶ 1.等值查询(主键)
SELECT*FROMusersWHEREid=100;- 路径:
- 根节点 → 非叶子节点 →定位到包含 id=100 的页
- 在页内二分查找具体行
▶ 2.范围查询(主键)
SELECT*FROMusersWHEREidBETWEEN100AND200;- 路径:
- 定位起始页 →顺序遍历双向链表直到结束
▶ 3.二级索引查询
SELECT*FROMusersWHEREname='Alice';- 路径:
- 在name 索引找到
(name='Alice', id=100) - 用
id=100回表到聚簇索引查完整行
- 在name 索引找到
五、避坑指南
| 陷阱 | 破局方案 |
|---|---|
| 认为“一行一节点” | 理解页是存储单位,单页含多行 |
| 忽略回表成本 | 高频查询用覆盖索引(避免回表) |
| 盲目增大页大小 | 16KB 是平衡 I/O 与内存的最优值 |
六、终极心法
**“B+ 树不是链条,
而是分块的仓库——
- 当你理解页结构,
你在校准存储;- 当你区分索引类型,
你在优化查询;- 当你计算行密度,
你在铸造性能。真正的数据库能力,
始于对页的敬畏,
成于对细节的精控。”
结语
从今天起:
- 用
SHOW TABLE STATUS查看行平均大小 - 高频查询设计覆盖索引(避免回表)
- 理解页分裂对写性能的影响
因为最好的索引设计,
不是盲目建索引,
而是精准控制每一比特的存储。