【实战】分析一张真实业务表的 InnoDB 存储结构

经过前面六篇文章的理论轰炸,我们已经对 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_timeidx_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_timeidx_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(如 INDEXUNDO_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 字符串,如 clickview 等。

找 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_IDDB_ROLL_PTR),支撑 MVCC;如果表没有主键,还会自动生成 DB_ROW_ID
  • 所有修改都会先记 Redo Log ,脏页通过 Doublewrite Buffer 安全刷盘,Checkpoint 控制日志空间和恢复速度。

通过 INFORMATION_SCHEMA 和物理工具的结合,我们可以从逻辑和物理两个维度透彻理解一张表。这种能力对于排查空间膨胀、索引碎片、以及极端情况下的数据恢复具有重要意义。


5. 小结

本篇实战我们完成了对一张真实业务表的全方位解剖:

  • 逻辑层面 :使用 INFORMATION_SCHEMA.INNODB_TABLESPACESINNODB_INDEXES 查询表空间大小、索引结构、根页位置。
  • 物理层面 :找到 .ibd 文件,用 py_innodb_page_info 统计页类型分布,用 hexdump 查看文件头和行记录的原始字节。
  • 总结:将表空间、段、区、页、行、隐藏列、索引 B+Tree 串成一张全景图,巩固了第四阶段全部知识。

第四阶段到此全部结束。从宏观架构到微观字节,你已经掌握了 InnoDB 内核的完整脉络。下一阶段,我们将进入 事务进阶、锁机制与并发控制,深入理解 InnoDB 如何在高并发下保证数据一致性的同时提供高性能。

扩展练习

  1. user_actions 表插入一条超过 8KB 的 log_text,然后用 py_innodb_page_info 查看是否出现 BLOB 页。
  2. 比较 COUNT(*) 在有多个二级索引和没有索引时的执行计划差异,思考索引的物理组织如何影响统计。
  3. hexdump 找到某条你知道数据的行记录,尝试识别出变长字段长度列表和隐藏列。

参考资料


相关推荐
Geoffwo1 小时前
Elasticsearch+IK+Kibana安装手册
大数据·elasticsearch·搜索引擎
超梦dasgg1 小时前
亿级数据 不停服务平滑迁移(生产环境实战方案)
java·数据库
郑洁文1 小时前
景点综合数据分析与应用
大数据·数据挖掘·数据分析·四川景点
Zella折耳根1 小时前
Java 正则表达式实战:IP 地址匹配与替换全解析
java·tcp/ip·正则表达式
摇滚侠1 小时前
JavaWeb 全套教程 Filter 107-111
java·开发语言·servlet
YIN_尹1 小时前
【Linux系统编程】基础IO第一讲——系统文件IO
android·java·linux·c++
凤山老林1 小时前
81-Java Scanner 类
java·开发语言
j_xxx404_1 小时前
MySQL数据库基础硬核解析:从 C/S 网络服务到磁盘文件与存储引擎
linux·运维·服务器·开发语言·数据库·mysql·ai
我是大猴子1 小时前
死锁,慢sql排查,mysql死锁
数据库·sql