1. B+ Tree 与物理数据页的关系
1.1 B+ Tree 索引的物理存储
InnoDB 中,B+ Tree 索引的每个节点就是一个物理数据页(16KB)。非叶子节点存储索引键和子页指针,叶子节点存储完整的数据行。
flowchart TD
subgraph BTree[B+ Tree 物理存储]
ND1["非叶子节点页
索引页
存储键+指针"]
ND2["非叶子节点页
索引页
存储键+指针"]
ND3["非叶子节点页
索引页"]
L1["叶子节点页
数据页
Row 1"]
L2["叶子节点页
数据页
Row 2"]
L3["叶子节点页
数据页
Row 3"]
L4["叶子节点页
数据页
Row 4"]
end
ND1 --> ND2
ND1 --> ND3
ND2 --> L1
ND2 --> L2
ND3 --> L3
ND3 --> L4
L1 -.-> L2
L2 -.-> L3
L3 -.-> L4
style ND1 fill:#e3f2fd,stroke:#1565c0
style ND2 fill:#e3f2fd,stroke:#1565c0
style ND3 fill:#e3f2fd,stroke:#1565c0
style L1 fill:#c8e6c9,stroke:#2e7d32
style L2 fill:#c8e6c9,stroke:#2e7d32
style L3 fill:#c8e6c9,stroke:#2e7d32
style L4 fill:#c8e6c9,stroke:#2e7d32
-- B+ Tree 节点 = 物理数据页 (16KB)
-- 非叶子节点(索引页)
-- 存储结构:[主键值, 页指针]
-- 每行约 12 字节
-- 每页可存:16KB / 12 ≈ 1365 个键
-- 叶子节点(数据页)
-- 存储结构:[主键, 所有列数据]
-- 每行大小取决于字段类型和数量
-- 每页可存:几百到几千行不等
-- 示例:B+ Tree 高度为 3
-- 根节点 -> 1365 个子节点 -> 1365² 行数据
-- 约可存储:1365 × 1365 × 1000 ≈ 18 亿行
1.2 索引查找与磁盘IO
每次通过索引查找数据,都需要将数据页从磁盘读取到 Buffer Pool。磁盘IO是数据库的主要性能瓶颈。
flowchart TD
A[查询 id=100] --> B[加载根节点页]
B --> C{页在Buffer Pool?}
C -->|否| D[磁盘IO: 读取ibd文件]
D --> E[放入Buffer Pool]
C -->|是| E
E --> F[在内存中查找]
F --> G[定位到子页指针]
G --> H[加载叶子节点页]
H --> I{页在Buffer Pool?}
I -->|否| J[磁盘IO: 读取ibd]
I -->|是| K
J --> K[放入Buffer Pool]
K --> L[找到目标行]
L --> M[返回结果]
D -->|1次IO| B
J -->|2次IO| H
M --> N[可能需要回表]
style D fill:#ffcdd2,stroke:#c62828
style J fill:#ffcdd2,stroke:#c62828
style E fill:#c8e6c9,stroke:#2e7d32
style K fill:#c8e6c9,stroke:#2e7d32
-- 索引查找的磁盘IO过程:
-- 1. 加载根节点页(第1次IO)
-- - 从ibd文件读取根节点页到Buffer Pool
-- - 根据查询键值确定下一级子页
-- 2. 加载中间节点页(第2次IO)
-- - 继续定位,直到叶子节点
-- 3. 加载叶子节点页(第3次IO)
-- - 叶子节点存储实际数据行
-- - 在页内查找目标记录
-- 4. 回表(如需)(第4次IO)
-- - 如果是覆盖索引,无需回表
-- - 否则需要根据主键再查一次
-- 性能指标:
-- - 磁盘随机IO延迟:~10ms
-- - 内存访问延迟:~100ns
-- - 速度差异:10万倍
-- - 因此:减少磁盘IO是优化关键
1.3 索引写入与页分裂
当插入新数据时,如果目标叶子节点已满,InnoDB 会进行页分裂操作,将一半数据移动到新页面。
flowchart TD
A[插入新记录] --> B{叶子节点已满?}
B -->|否| C[直接插入]
B -->|是| D[页分裂]
D --> E[创建新叶子节点]
E --> F[移动一半数据到新节点]
F --> G[更新父节点指针]
G --> H[插入新记录]
C --> I[可能触发父节点分裂]
H --> I
I --> J[结束]
style B fill:#fff3e0,stroke:#e65100
style D fill:#ffcdd2,stroke:#c62828
style E fill:#ffcdd2,stroke:#c62828
style F fill:#ffcdd2,stroke:#c62828
style G fill:#c8e6c9,stroke:#2e7d32
style C fill:#c8e6c9,stroke:#2e7d32
-- 页分裂详解:
-- 触发条件:叶子节点数据页满(16KB)
-- 分裂过程:
-- 1. 分配新叶子节点页
-- 2. 将原页一半记录移动到新页
-- 3. 在父节点添加新指针
-- 4. 插入新记录
-- 页分裂的性能影响:
-- - 需要分配新页面
-- - 需要修改多个数据页
-- - 可能引发连锁分裂(向上传播)
-- - 导致页稀疏,空间利用率下降
-- 优化建议:
-- - 使用自增主键,顺序插入避免分裂
-- - 避免随机主键(UUID等)
-- - 批量插入减少分裂次数
1.4 B+ Tree 操作与物理IO对照表
| 操作类型 |
B+ Tree 动作 |
物理IO |
说明 |
| 点查 |
定位叶子节点 |
树高度次 |
如 WHERE id = 100 |
| 范围查询 |
定位起点 + 顺序扫描 |
树高度 + 扫描页数 |
如 WHERE id > 100 |
| 顺序扫描 |
从最左叶子开始 |
全表页数 |
无WHERE条件 |
| 插入 |
查找 + 可能的分裂 |
树高度 + 分裂次数 |
可能触发多次IO |
| 更新 |
查找 + 修改叶子 |
树高度 + 脏页刷新 |
涉及Redo Log |
| 删除 |
查找 + 标记删除 |
树高度 + 可能的合并 |
后台Purge清理 |
2. 数据页结构详解
2.1 数据页 16KB 内部结构
flowchart TD
subgraph Page[InnoDB 数据页 16KB]
FH["File Header
38字节
页ID、checksum"]
PH["Page Header
56字节
页目录信息"]
INF["Infimum
13字节
虚拟记录"]
SUP["Supremum
13字节
虚拟记录"]
UR["User Records
用户记录区
(实际数据)"]
FS["Free Space
空闲空间"]
PD["Page Directory
页目录
稀疏索引"]
FT["File Trailer
8字节
校验信息"]
end
FH --> PH
PH --> INF
INF --> SUP
SUP --> UR
UR --> FS
FS --> PD
PD --> FT
-- 16KB 数据页结构分解:
-- 1. File Header (38字节)
-- - 页ID (space ID + page number)
-- - 上一页/下一页指针(双向链表)
-- - 页类型 (INDEX/SYSTEM/UNDO等)
-- - LSN (日志序列号)
-- - Checksum (数据完整性校验)
-- 2. Page Header (56字节)
-- - 已记录数 (PAGE_N_DIR_SLOTS)
-- - 空闲空间起始位置 (PAGE_FREE)
-- - 第一个被删除记录的位置
-- - 页面中记录数量
-- 3. Infimum + Supremum (各13字节)
-- - 虚拟记录,作为页内记录边界
-- - Infimum: 比任何用户记录都小
-- - Supremum: 比任何用户记录都大
-- 4. User Records (用户数据区)
-- - 实际存储的行数据
-- - 变长字段使用额外空间存储长度
-- 5. Free Space (空闲区)
-- - 未使用的空间
-- - 新记录从这里分配
-- 6. Page Directory (页目录)
-- - 稀疏索引,指向记录组
-- - 二分查找加速定位
-- 7. File Trailer (8字节)
-- - 页尾校验
-- - 检测页面是否损坏
2.2 数据页内查找机制
-- 数据页内查找过程:
-- 1. Page Directory 二分查找
-- - 将页内记录分组,每组4-8条
-- - 每组一个目录项(2字节指针)
-- - 二分查找定位到组
-- 2. 顺序查找精确定位
-- - 在目标组内顺序查找
-- - 组内记录数少,扫描开销小
-- 示例:
-- - 假设页内有1000条记录
-- - 每组8条,需要125个目录项
-- - 二分查找:log2(125) = 7次
-- - 组内查找最多8次
-- - 总计:约15次比较定位记录
-- 对比:
-- - 无页目录:1000次线性比较
-- - 有页目录:15次比较
-- - 性能提升:60倍
3. ibd 文件存储结构
3.1 ibd 文件物理布局
flowchart TD
subgraph IBD[ibd 文件结构]
H1["Space Header
第1页
表空间头信息"]
H2["FSP Header
第2页
空闲空间管理"]
XDES["XDES
第3-64页
区描述符"]
IBUF["Insert Buffer
B+ Tree
索引页"]
DICT["Data Dictionary
B+ Tree
数据字典"]
IDX["Index B+ Tree
索引+数据
主要存储区"]
UNDO["Undo Log
回滚日志
历史版本"]
end
H1 --> H2
H2 --> XDES
XDES --> IBUF
XDES --> DICT
XDES --> IDX
XDES --> UNDO
-- ibd 文件页分配结构:
-- 第1页:Space Header (Tablespace Header)
-- - 表空间ID、标志位
-- - 第一个可用Extent地址
-- - 所有数据页的bitmap
-- 第2页:FSP Header (File Space Header)
-- - 空闲Extent链表
-- - 完全使用Extent链表
-- - 碎片Extent信息
-- 第3-64页:XDES (Extent Descriptor)
-- - 每个Extent描述符256字节
-- - 每个Extent (64页) 占4字节
-- - 描述每个Extent的状态
-- 索引页区域:
-- - B+ Tree 非叶子节点
-- - B+ Tree 叶子节点(数据)
-- - 按Extent分配和管理
-- 特殊页:
-- - Insert Buffer B+ Tree
-- - Data Dictionary B+ Tree
-- - Undo Log 页
3.2 区(Extent)与段(Segment)
flowchart TD
subgraph Pages[数据页]
P1["页 1"] --> P2["页 2"]
P2 --> P3["页 3"]
P3 --> P64["页 64"]
end
subgraph Extents[区]
E1["Extent 1"] --> E2["Extent 2"]
E2 --> E3["Extent N"]
end
style P1 fill:#e3f2fd,stroke:#1565c0
style E1 fill:#c8e6c9,stroke:#2e7d32
-- Extent(区)结构:
-- - 1 Extent = 64 连续页 = 1MB
-- - 顺序IO优化:减少磁盘寻道
-- - 空间预分配:提高写入性能
-- Segment(段)结构:
-- - Leaf Node Segment:叶子节点段
-- - Non-Leaf Node Segment:非叶子节点段
-- - 每个索引由两个Segment组成
-- Extent 分配策略:
-- 1. 碎片页分配 (0-32页)
-- - 按需分配单页
-- - 空间利用率高
-- - 随机IO较多
-- 2. 完整区分配 (>32页)
-- - 整区1MB分配
-- - 顺序IO优化
-- - 空间可能浪费
-- 查看表空间使用:
SELECT
name,
total_pages,
total_pages * 16384 / 1024 / 1024 AS size_mb
FROM information_schema.innodb_tablespaces
WHERE name = 'database/table_name';
3.3 表空间类型
-- InnoDB 表空间类型:
-- 1. 系统表空间 (System Tablespace)
-- 文件:ibdata1, ibdata2, ...
-- 默认路径:innodb_data_home_dir
-- 存储内容:
-- - data dictionary (数据字典)
-- - undo log (回滚日志)
-- - change buffer (变更缓冲)
-- - doublewrite buffer (双写缓冲)
-- - 所有表的元数据和索引
-- 2. 独立表空间 (File-Per-Table)
-- 文件:database/table_name.ibd
-- 优点:
-- - 方便单表备份
-- - 支持表级压缩
-- - 删除表可立即回收空间
-- - I/O隔离(可单独flush)
-- 配置:innodb_file_per_table = ON
-- 3. 通用表空间 (General Tablespace)
-- 文件:自定义 .ibd 文件
-- 优点:
-- - 多表共享表空间
-- - 减少文件系统碎片
-- - 支持大表空间
CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd';
CREATE TABLE t1 (id INT) TABLESPACE ts1;
-- 4. 临时表空间 (Temporary Tablespace)
-- 文件:ibtemp0, ibtemp1
-- 用途:临时表、排序结果
-- 特点:重启后重建
-- 5. Undo 表空间
-- 文件:undo_001, undo_002
-- 用途:事务回滚
4. Buffer Pool 与数据页读写
Buffer Pool 核心概念
| 指标 |
数值 |
说明 |
| 内存访问 |
~100ns |
纳秒级,极快 |
| 磁盘IO |
~10ms |
毫秒级,相差10万倍 |
| 建议配置 |
50-70% |
可用内存比例 |
| 页大小 |
16KB |
与数据页一致 |
Buffer Pool 工作原理
flowchart TD
subgraph Read[读取流程]
R1[查询数据] --> R2{命中Buffer Pool?}
R2 -->|Yes| R3[直接返回 ~100ns]
R2 -->|No| R4[磁盘读取 ~10ms]
R4 --> R5[放入Buffer Pool]
R5 --> R3
end
subgraph Write[写入流程]
W1[修改数据] --> W2[修改Buffer Pool页]
W2 --> W3[标记为脏页]
W3 --> W4[后台刷新到磁盘]
end
style R3 fill:#c8e6c9,stroke:#2e7d32
style R4 fill:#ffcdd2,stroke:#c62828
style W2 fill:#fff3e0,stroke:#e65100
LRU 淘汰策略
| 区域 |
比例 |
说明 |
| New Sublist |
~37% |
热数据区,最近访问 |
| Old Sublist |
~63% |
冷数据区,较早访问 |
| midpoint |
位置 |
新页插入的分界点 |
Buffer Pool 配置参数
-- 核心配置参数
-- 1. Buffer Pool 大小(建议50-70%可用内存)
SET GLOBAL innodb_buffer_pool_size = 4 * 1024 * 1024 * 1024; -- 4GB
-- 2. Buffer Pool 实例数(减少锁竞争)
SET GLOBAL innodb_buffer_pool_instances = 4;
-- 3. 冷数据区比例(默认37%)
SET GLOBAL innodb_old_blocks_pct = 37;
-- 4. 预热时间(访问后多久进入热区)
SET GLOBAL innodb_old_blocks_time = 1000; -- 1000ms
-- 5. 脏页刷新策略
SET GLOBAL innodb_max_dirty_pages_pct = 75; -- 超过比例强制刷新
-- 6. 查看状态
SHOW ENGINE INNODB STATUS;
SHOW VARIABLES LIKE 'innodb_buffer_pool%';
Buffer Pool 性能监控
| 指标 |
计算公式 |
理想值 |
| 命中率 |
(1 - Key_reads/Key_requests) * 100% |
> 99% |
| 脏页比例 |
Pages_dirty / Pages_total * 100% |
< 10% |
| 读取效率 |
Key_reads / Key_requests |
< 0.01 |
脏页刷新机制
flowchart TD
A[数据修改] --> B[修改Buffer Pool页]
B --> C{innodb_flush_log_at_trx_commit?}
C -->|1| D[立即写Redo Log]
C -->|2| E[每秒写Redo Log]
C -->|0| F[延迟写Redo Log]
D --> G[后台刷新线程]
E --> G
F --> G
G --> H{刷新策略}
H -->|Page Cleaner| I[刷新脏页到ibd]
I --> J[干净页]
style B fill:#fff3e0,stroke:#e65100
style I fill:#c8e6c9,stroke:#2e7d32
-- 脏页刷新机制:
-- 刷新策略 (innodb_flush_log_at_trx_commit):
-- 1. = 1 (默认,最安全)
-- 每次事务提交同步写Redo Log
-- 最多丢失1个事务
-- 2. = 2
-- 每次事务提交写OS缓存
-- 每秒刷新到磁盘
-- 最多丢失1秒数据
-- 3. = 0
-- 不立即写Redo Log
-- 每秒刷新
-- 最多丢失1秒数据(危险!)
-- 刷新时机:
-- a. 脏页比例超过阈值
-- innodb_max_dirty_pages_pct = 75%
-- b. Redo Log空间不足
-- c. 后台Page Cleaner线程定时刷新
-- d. 正常关闭数据库
-- 查看Buffer Pool状态:
SHOW ENGINE INNODB STATUS;
-- 查看:Buffer pool size, Pages dirty, Pages read/write
-- 性能影响:
-- - 脏页过多:恢复时间长
-- - 刷新过慢:磁盘IO突增
-- - 刷新过快:IO争用
预读机制
-- InnoDB 预读 (Read Ahead):
-- 1. 线性预读 (Linear Read Ahead)
-- 条件:顺序访问同一Extent的多个页
-- 触发:innodb_read_ahead_threshold = 56 (默认)
-- 行为:预读整个Extent (64页)
-- 示例:全表扫描时自动触发
-- 2. 随机预读 (Random Read Ahead)
-- 条件:同一Extent内多个页被访问
-- 触发:innodb_random_read_ahead = ON
-- 行为:预读整个Extent
-- 注意:默认关闭,可能增加IO
-- 预读触发示例:
-- SELECT * FROM users ORDER BY id;
-- -- 顺序扫描,触发线性预读
-- SELECT * FROM users WHERE id IN (1,100,200,300);
-- -- 可能触发随机预读
-- 配置优化:
-- innodb_read_ahead_threshold = 64
-- innodb_random_read_ahead = OFF
-- innodb_read_io_threads = 4
5. 空间管理与回收
空间分配流程
-- InnoDB 空间分配流程:
-- 1. 请求分配新页
-- a. 检查Fragment Array (前32页)
-- b. 有碎片页则分配单页
-- c. 无碎片页则分配完整Extent
-- 2. Extent 分配
-- a. 从FSP_FREE链表中获取
-- b. 标记为Fragment Array使用
-- c. 32页后标记为完整Extent
-- 3. 数据页分配
-- a. 从Fragment Array分配
-- b. 初始化页头
-- c. 加入LRU链表
-- 4. 索引结构维护
-- a. 更新B+ Tree结构
-- b. 维护Page Directory
-- c. 更新数据字典
-- 查看表空间统计:
SELECT
table_name,
round(table_rows / 10000, 1) AS rows_w,
round(data_length / 1024 / 1024, 1) AS data_mb,
round(index_length / 1024 / 1024, 1) AS index_mb
FROM information_schema.tables
WHERE engine = 'InnoDB';
空间回收机制
-- 空间回收机制:
-- 1. 删除记录的逻辑删除
-- - 记录标记为"已删除"
-- - 空间标记为可复用
-- - 真正删除由Purge线程处理
-- 2. Purge 机制
-- - 后台线程定期清理已删除记录
-- - 清理Undo Log
-- - 释放空间到Free List
-- - innodb_purge_threads = 4
-- 3. 页面合并
-- - 当相邻页利用率低时合并
-- - 释放多余页到Free Extent
-- - 减少碎片
-- 4. 手动回收空间
-- OPTIMIZE TABLE table_name;
-- -- 重建表,整理碎片
-- ALTER TABLE t ENGINE=InnoDB;
-- -- 在线重建表
-- ALTER TABLE t TABLESPACE=innodb_file_per_table;
-- -- 移动到独立表空间
-- 注意事项:
-- - ibd文件不会自动缩小
-- - DELETE只标记删除,不释放空间
-- - TRUNCATE会重建表,释放空间
常见问题与优化
-- 常见空间问题:
-- 问题1:ibd文件过大
-- 原因:大量删除后空间未回收
-- 解决:OPTIMIZE TABLE重建表
-- 问题2:碎片过多
-- 原因:随机插入导致页分裂频繁
-- 解决:使用自增主键,定期优化
-- 问题3:Buffer Pool命中率低
-- 原因:数据量超过Buffer Pool大小
-- 解决:增加Buffer Pool,优化查询
-- 监控SQL:
-- -- 查看表大小
-- SELECT table_name,
-- round(data_length/1024/1024,2) as mb
-- FROM information_schema.tables
-- WHERE table_schema = 'db';
-- -- 查看索引统计
-- SHOW INDEX FROM table_name;
-- -- 查看InnoDB状态
-- SHOW ENGINE INNODB STATUS\G
-- -- 查看脏页比例
-- SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';