在上一篇中,我们俯瞰了 MySQL 的四层架构,并认识到 InnoDB 是当今绝对的存储引擎主力。今天我们将带上放大镜,深入到 InnoDB 内部物理存储结构的"细胞"级别------看看数据在磁盘上究竟是怎么摆放的。理解这些概念,是后续精通 InnoDB 内存结构、日志系统和崩溃恢复的基石。
本文将逐层展开以下内容:
- 表空间(Tablespace)的分类与结构
- 段(Segment)、区(Extent)、页(Page)的层级关系
- InnoDB 页的默认大小(16KB)及其意义
- 行格式(Row Format)的发展与对比
- 如何查看表空间和行格式信息
- 实战:创建带不同行格式的表并观察存储文件
1. 表空间:数据的容器
InnoDB 中,所有数据都被逻辑地组织在**表空间(Tablespace)**中。表空间是一个抽象容器,里面装着表的数据和索引。根据管理方式不同,表空间主要分为以下三种。
1.1 系统表空间(System Tablespace)
系统表空间对应磁盘上的一个或多个文件(通常是 ibdata1),它存放着:
- 数据字典 :表结构、列信息等元数据(在 MySQL 8.0 中,数据字典已迁移到独立的
mysql.ibd文件,但老的版本仍在这里)。 - 变更缓冲区(Change Buffer):优化二级索引写入。
- 双写缓冲区(Doublewrite Buffer):保证页的完整写入。
- Undo 日志:在 MySQL 5.6 之前默认存在这里,之后可以独立出来。
可以通过参数 innodb_data_file_path 配置文件大小和自动扩展属性:
ini
innodb_data_file_path=ibdata1:12M:autoextend
1.2 独立表空间(File-Per-Table Tablespace)
这是最常用的模式。开启 innodb_file_per_table(默认 ON)后,每张表都会在数据目录下生成一个独立的 .ibd 文件,存放该表的数据和索引。这种设计带来了巨大便利:
- 删除或清空表时,磁盘空间可以被操作系统回收(系统表空间则不会缩小)。
- 可以单独备份或恢复某张表。
- 方便在线迁移表(
ALTER TABLE ... IMPORT/DISCARD TABLESPACE)。
你可以在数据目录里看到这些文件:
bash
ls /var/lib/mysql/library_db/
# books.ibd readers.ibd borrow_records.ibd ...
1.3 通用表空间(General Tablespace)
MySQL 5.7 引入的共享表空间,可以手动创建并指定名称和文件。多张表可以放在同一个通用表空间中,比系统表空间更灵活。
sql
CREATE TABLESPACE my_space ADD DATAFILE 'my_space.ibd' ENGINE=InnoDB;
CREATE TABLE t1 (id INT) TABLESPACE my_space;
1.4 其他
- 临时表空间(Temporary Tablespace):存储临时表的数据,重启后数据清空。
- Undo 表空间(Undo Tablespace) :专门存放 Undo 日志,当
innodb_undo_tablespaces大于 0 时使用独立文件。
2. 段、区、页:从宏观到微观的层级
表空间内部被划分为若干个层级结构,这是 InnoDB 管理存储空间的核心逻辑。
2.1 页(Page) ------ 最小的 I/O 单位
页是 InnoDB 的最小存储和 I/O 单元。默认大小为 16KB (可通过 innodb_page_size 配置为 4KB/8KB/16KB/32KB/64KB,但初始化后不可更改)。每次读写磁盘,InnoDB 都是整页整页操作的。一页中可能存放多条记录,也可能作为索引节点存放键值。
页的类型很多,常见的有:
- 数据页(Index Page):存放实际行记录。
- Undo 页:存放 Undo 日志。
- 系统页:存放事务信息、段信息等。
2.2 区(Extent) ------ 连续页的组
一个区由 64 个连续的页 组成,因此区的大小是 16KB × 64 = 1MB。区存在的意义是保证在磁盘上尽可能连续分配空间,减少随机 I/O,特别是在全表扫描或范围查询时。
如果表很小(< 1MB),InnoDB 会先从系统表空间的碎片页中分配(以减小空间浪费),当表大小超过 1MB 后,才开始以区为单位分配。
2.3 段(Segment) ------ 逻辑上的数据集合
段是一个逻辑概念,它由若干个区组成。一张 InnoDB 表至少包含两种段:
- 数据段(Leaf Node Segment):存放 B+Tree 的叶子节点(即实际的行数据)。
- 索引段(Non-Leaf Node Segment):存放 B+Tree 的非叶子节点(索引目录)。
对于建有二级索引的表,每个索引也有自己独立的索引段。
2.4 一张图总结层级关系(文字版)
表空间 (Tablespace / .ibd 文件)
├── 段 (Segment) ------ 数据段
│ ├── 区 (Extent) ------ 1MB (64页)
│ │ ├── 页 (Page) ------ 16KB
│ │ │ └── 行 (Row) ------ 实际记录
│ │ ├── 页 (Page)
│ │ └── ...
│ └── 区 (Extent)
└── 段 (Segment) ------ 索引段
└── ...
理解这个层级,就能明白为什么 InnoDB 在顺序读主键时性能很好------页在区中连续,区又在段中尽量连续,磁头能够高效地一次性读取大片数据。
3. 行格式:一行的物理表示
一页里面到底怎么存放每一行记录?这就是"行格式"决定的事。InnoDB 先后引入了四种行格式,目的是在存储效率、读写性能和功能之间取得平衡。
3.1 REDUNDANT ------ 最古老的格式
这是 MySQL 4.x 时代的遗留格式,现在几乎不用。它存储了很多冗余信息,占用空间大,缺乏变长字段的高效处理。
3.2 COMPACT ------ 紧凑的基础格式
MySQL 5.0 引入,在空间效率上做了优化。结构大致如下:
┌─────────────────────┐
│ 变长字段长度列表 │ ← 记录 VARCHAR 等列的实际长度
├─────────────────────┤
│ NULL 标志位 │ ← 标记哪些列是 NULL
├─────────────────────┤
│ 记录头信息 (5字节) │ ← 如记录类型、指向下一记录的指针等
├─────────────────────┤
│ 列数据 (按建表顺序) │ ← 实际的数据值
└─────────────────────┘
COMPACT 格式已经能较好支持变长字符集(如 UTF8MB4)和 NULL 值,但对于 BLOB/TEXT 大字段,前 768 字节会存在行内(溢出页指针放在行内),其余数据放到溢出页中。
3.3 DYNAMIC ------ 当前的默认格式
MySQL 5.7 开始默认使用 DYNAMIC 行格式。它和 COMPACT 非常相似,核心区别在于对大字段的处理:DYNAMIC 只会在行内存放一个 20 字节的溢出指针,实际数据完全放到溢出页中(而 COMPACT 还会在行内保留 768 字节前缀)。这使得 DYNAMIC 在处理 BLOB/TEXT 时更加灵活,也支持更大的索引前缀。
3.4 COMPRESSED ------ 压缩格式
在 DYNAMIC 的基础上增加了页级压缩,可以节省磁盘空间,但需要消耗额外的 CPU 进行压缩/解压。通过 KEY_BLOCK_SIZE 控制压缩后的页大小(如 1/2/4/8KB)。
3.5 查看与设置行格式
查看表的行格式:
sql
SHOW TABLE STATUS LIKE 'books'\G
-- 关注 Row_format 字段
或者查询 INFORMATION_SCHEMA:
sql
SELECT TABLE_NAME, ROW_FORMAT FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'library_db';
在建表时指定:
sql
CREATE TABLE test_dynamic (
id INT PRIMARY KEY,
content TEXT
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
修改现有表的行格式:
sql
ALTER TABLE test_dynamic ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
这个操作会重建整张表,大表请谨慎在线操作。
4. 实战:观察 .ibd 文件与行格式
4.1 创建测试表
sql
USE library_db;
CREATE TABLE format_test (
id INT AUTO_INCREMENT PRIMARY KEY,
short_text VARCHAR(100),
long_text TEXT,
bin_data BLOB
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
-- 插入一些数据
INSERT INTO format_test (short_text, long_text, bin_data) VALUES
('hello', REPEAT('A', 100), X'CAFEBABE'),
('world', REPEAT('B', 5000), NULL),
(NULL, NULL, REPEAT('C', 2000));
4.2 找到 .ibd 文件
找到 MySQL 数据目录(可通过 SHOW VARIABLES LIKE 'datadir' 查看):
bash
cd /var/lib/mysql/library_db
ls -lh format_test.ibd
你会看到一个 format_test.ibd 文件,大小随着插入数据而增长。
4.3 使用 hexdump 查看(可选)
如果你想亲眼看看物理存储的二进制形态,可以用 hexdump 查看文件头部(需要操作系统访问权限):
bash
hexdump -C format_test.ibd | head -n 20
文件头部包含表空间 ID、LSN(日志序列号)等关键信息。这种底层探索对理解 InnoDB 非常有帮助,我们后续在页结构篇还会深入。
4.4 对比行格式的占用空间
我们可以新建一个同样结构的表,使用不同的行格式,插入同样数据,粗略对比 .ibd 文件的大小:
sql
-- COMPACT
CREATE TABLE format_compact LIKE format_test;
ALTER TABLE format_compact ROW_FORMAT=COMPACT;
INSERT INTO format_compact SELECT * FROM format_test;
-- COMPRESSED
CREATE TABLE format_compressed LIKE format_test;
ALTER TABLE format_compressed ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
INSERT INTO format_compressed SELECT * FROM format_test;
然后在操作系统中比较 .ibd 文件:
bash
ls -lh format_*.ibd
通常情况下,COMPRESSED 的文件会小一些,DYNAMIC 与 COMPACT 对于非大字段表的差异不大。
5. 小结
今天我们下潜到了 InnoDB 的物理存储世界:
- 表空间 是最高层容器,分为系统表空间、独立表空间(每表一个
.ibd)、通用表空间等。生产环境推荐使用独立表空间。 - 表空间内部按 段 → 区 → 页 层级组织。区是 1MB 连续空间(64 页),页是 16KB 的 I/O 最小单位。
- 每张 InnoDB 表至少有两个段:数据段(叶子节点)和索引段(非叶子节点)。
- 行格式 决定了页内每一行的物理表示。
DYNAMIC是当前默认,对大字段的处理更优;COMPACT是前一代;REDUNDANT已淘汰;COMPRESSED用 CPU 换空间。 - 我们可以通过
SHOW TABLE STATUS和SHOW VARIABLES查看行格式与文件配置,也可以在 OS 层面观察.ibd文件。
至此,你已经能看到 InnoDB 从宏观到微观的物理轮廓。下一篇文章,我们将更近一步,打开一个页,解剖其内部结构(页头、页目录、行记录、文件尾),并认识隐藏列和 LSN,为理解索引和事务原理打下坚实的基础。
思考题:
- 为什么区的大小设计为 1MB?与磁盘 I/O 有什么关系?
- 如果一个表有大量 TEXT/BLOB 列,DYNAMIC 和 COMPACT 格式在查询性能上可能有什么差异?
- 在数据目录中看看你自己的
library_db表文件,它们的大小与表内数据量吻合吗?
参考资料
- MySQL 8.0 Reference Manual - InnoDB Physical Structures
- MySQL 8.0 Reference Manual - InnoDB Tablespaces
- MySQL 8.0 Reference Manual - InnoDB Row Formats