经过前面六篇文章的理论轰炸,我们已经对 InnoDB 的表空间、段、区、页、行格式、内存架构以及日志系统有了全面的认识。但"纸上得来终觉浅",今天我们将拿起工具,亲手解剖一张真实的 InnoDB 表,从逻辑信息到物理字节,把第四阶段学到的所有知识串联起来。
本文将带你完成以下实战任务:
- 创建一张包含多种数据类型的"用户行为日志"表
- 使用
INFORMATION_SCHEMA查询表空间、页、行格式等逻辑信息 - 使用
py_innodb_page_info工具分析.ibd文件,了解页的类型分布 - 用
hexdump观察文件头和行记录的原始字节 - 总结 InnoDB 数据的物理组织全景图
1. 准备测试表:模拟真实业务
我们设计一张典型的"用户行为日志"表,它包含了数值、字符串、日期时间、大文本等多种数据类型,并且建有二级索引。
sql
CREATE DATABASE IF NOT EXISTS innodb_lab;
USE innodb_lab;
CREATE TABLE user_actions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
action_type ENUM('click','view','purchase','comment') NOT NULL,
target_id BIGINT UNSIGNED DEFAULT NULL,
metadata JSON DEFAULT NULL,
log_text TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, created_at),
INDEX idx_action_type (action_type)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
表结构特点:
id是自增主键(聚簇索引)。metadata是 JSON 类型(底层存储为 BLOB)。log_text是 TEXT 类型,可能产生溢出页。- 有两个二级索引
idx_user_time和idx_action_type。
插入 500 条模拟数据,使表有足够的页来分析:
sql
INSERT INTO user_actions (user_id, action_type, target_id, metadata, log_text)
SELECT
FLOOR(1 + RAND() * 50),
ELT(FLOOR(1 + RAND() * 4), 'click', 'view', 'purchase', 'comment'),
FLOOR(1 + RAND() * 200),
JSON_OBJECT('ip', CONCAT('192.168.', FLOOR(RAND()*255), '.', FLOOR(RAND()*255)),
'duration', FLOOR(RAND() * 1000)),
CONCAT('用户于 ', NOW() - INTERVAL FLOOR(RAND() * 30) DAY, ' 执行了操作')
FROM (
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 500
)
SELECT n FROM seq
) nums;
2. 逻辑层探查:INFORMATION_SCHEMA
在解析物理文件之前,我们先从 MySQL 内部字典获取表空间和索引的"逻辑"信息。
2.1 查看表空间基本信息
sql
SELECT
NAME AS tablespace_name,
FILE_SIZE,
ALLOCATED_SIZE
FROM INFORMATION_SCHEMA.INNODB_TABLESPACES
WHERE NAME = 'innodb_lab/user_actions'\G
如果该表使用独立表空间,NAME 格式为 数据库/表名。FILE_SIZE 是当前 .ibd 文件大小,ALLOCATED_SIZE 是实际分配的磁盘空间。
2.2 查看索引信息
sql
SELECT
INDEX_ID,
NAME AS index_name,
TYPE,
NUM_ROWS,
SPACE,
PAGE_NO
FROM INFORMATION_SCHEMA.INNODB_INDEXES
WHERE TABLE_ID = (
SELECT TABLE_ID FROM INFORMATION_SCHEMA.INNODB_TABLES
WHERE NAME = 'innodb_lab/user_actions'
);
你会看到三行:PRIMARY(聚簇索引)、idx_user_time 和 idx_action_type(二级索引)。每个索引都有一个唯一的 INDEX_ID,还有所在表空间 ID(SPACE)和根页号(PAGE_NO,通常为 3 或 4)。
2.3 查看索引的物理页信息(可选)
sql
SELECT *
FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
WHERE TABLE_NAME = '`innodb_lab`.`user_actions`'
LIMIT 10;
这会列出 Buffer Pool 中缓存的该表的页,包括 PAGE_TYPE(如 INDEX、UNDO_LOG 等)和 PAGE_NUMBER。
3. 物理层分析:解析 .ibd 文件
逻辑信息看完,我们进入最激动人心的环节------直接读取 .ibd 文件。
3.1 找到 .ibd 文件
先确定数据目录:
sql
SHOW VARIABLES LIKE 'datadir';
假设 /var/lib/mysql/,那么文件路径为:
bash
/var/lib/mysql/innodb_lab/user_actions.ibd
3.2 使用 py_innodb_page_info 分析页类型
py_innodb_page_info 是 Jeremy Cole 开发的 Python 工具,能够快速扫描 .ibd 文件,统计每种类型的页及其数量。可以从 GitHub 获取(项目地址:https://github.com/jeremycole/innodb_ruby,或直接搜索 py_innodb_page_info.py)。
用法:
bash
python py_innodb_page_info.py /var/lib/mysql/innodb_lab/user_actions.ibd
示例输出:
Total number of page: 12
Insert Buffer Bitmap: 1
System Page: 0
Transaction system Page: 0
Freshly Allocated Page: 1
B-tree Node: 8
File Space Header: 1
Extent descriptor page: 1
Uncompressed BLOB Page: 0
解读:
- File Space Header(空间头):第 0 页,存放表空间元信息。
- Insert Buffer Bitmap:第 1 页,Change Buffer 的位图(即使无变化也存在)。
- B-tree Node:占大多数,是真正存储数据和索引的页(包括聚簇索引和二级索引)。
- Uncompressed BLOB Page:溢出页,如果 TEXT/BLOB 列数据太大而单独存储会看到。
如果你的表没有大字段溢出,这里可能没有 BLOB 页。我们可以故意插入一条超长文本触发溢出看看效果,但非必须。
3.3 使用 hexdump 手动观察
如果没有 Python 环境,也可以直接用 hexdump 查看文件头部和关键结构。
查看文件头(第 0 页):
bash
hexdump -C user_actions.ibd | head -60
关注:
- 前 4 字节:校验和
- 偏移 4~7:页号(
00 00 00 00表示第 0 页) - 偏移 24~27:表空间 ID(
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID)
定位数据页(如第 3 页,即根页) :
一页 16KB,第 3 页的偏移量为 3 * 16384 = 49152(即 0xC000)。我们可以用 hexdump 的 -s 选项跳过前面的页:
bash
hexdump -C -s 0xC000 user_actions.ibd | head -40
你可能会看到页头信息,以及行记录中的 ASCII 字符串,如 click、view 等。
找 Infimum 记录 :
在数据页中,Infimum 记录的文本标识是 infimum(十六进制 69 6e 66 69 6d 75 6d)。可以搜索:
bash
hexdump -C user_actions.ibd | grep "69 6e 66 69 6d 75 6d"
这会帮你定位数据页的开始部分。
4. 总结:InnoDB 物理组织全景图
结合本次实战,我们可以画出一张完整的逻辑层次图(用文字描述):
数据库实例
└── 数据库 (innodb_lab)
└── 表 (user_actions)
├── 聚簇索引 (PRIMARY) → 数据段
│ ├── 根页 (Page 3)
│ ├── 内部节点页 (B-tree Node)
│ └── 叶子节点页 (存储完整行,包含 DB_TRX_ID, DB_ROLL_PTR)
├── 二级索引 idx_user_time → 索引段
│ └── 叶子存(user_id, created_at, 主键)
├── 二级索引 idx_action_type → 索引段
└── 溢出页 (若 TEXT/JSON 过大)
- 每个索引占用一个独立的 B+Tree,根页位置记录在数据字典中。
- 叶子页之间通过双向链表连接(文件头中的
FIL_PAGE_PREV/FIL_PAGE_NEXT),实现高效范围扫描。 - 行记录中包含 隐藏列 (
DB_TRX_ID、DB_ROLL_PTR),支撑 MVCC;如果表没有主键,还会自动生成DB_ROW_ID。 - 所有修改都会先记 Redo Log ,脏页通过 Doublewrite Buffer 安全刷盘,Checkpoint 控制日志空间和恢复速度。
通过 INFORMATION_SCHEMA 和物理工具的结合,我们可以从逻辑和物理两个维度透彻理解一张表。这种能力对于排查空间膨胀、索引碎片、以及极端情况下的数据恢复具有重要意义。
5. 小结
本篇实战我们完成了对一张真实业务表的全方位解剖:
- 逻辑层面 :使用
INFORMATION_SCHEMA.INNODB_TABLESPACES、INNODB_INDEXES查询表空间大小、索引结构、根页位置。 - 物理层面 :找到
.ibd文件,用py_innodb_page_info统计页类型分布,用hexdump查看文件头和行记录的原始字节。 - 总结:将表空间、段、区、页、行、隐藏列、索引 B+Tree 串成一张全景图,巩固了第四阶段全部知识。
第四阶段到此全部结束。从宏观架构到微观字节,你已经掌握了 InnoDB 内核的完整脉络。下一阶段,我们将进入 事务进阶、锁机制与并发控制,深入理解 InnoDB 如何在高并发下保证数据一致性的同时提供高性能。
扩展练习:
- 为
user_actions表插入一条超过 8KB 的log_text,然后用py_innodb_page_info查看是否出现 BLOB 页。 - 比较
COUNT(*)在有多个二级索引和没有索引时的执行计划差异,思考索引的物理组织如何影响统计。 - 用
hexdump找到某条你知道数据的行记录,尝试识别出变长字段长度列表和隐藏列。
参考资料
- MySQL 8.0 Reference Manual - INFORMATION_SCHEMA InnoDB Tables
- Jeremy Cole's InnoDB Tools (innodb_ruby)
- MySQL Internals Manual - InnoDB Page Structure