一、行格式总览
1.1 四种行格式与默认值
| 行格式 | 出现时代 | 是否默认 | 总体定位 |
|---|---|---|---|
| REDUNDANT | InnoDB 最早(MySQL 4.1 时代) | ❌ | 历史遗留,仅兼容旧表 |
| COMPACT | MySQL 5.0 引入 | 5.6 默认 | 5.x 时代的主流格式 |
| DYNAMIC | MySQL 5.7 引入 | 5.7 / 8.0 默认 | 当前默认,长字段全量溢出 |
| COMPRESSED | MySQL 5.7 引入 | ❌ | DYNAMIC + 页级压缩,时间换空间 |
加粗要点 :8.0 默认行格式是 DYNAMIC ,由系统变量 innodb_default_row_format 控制(默认值 DYNAMIC)。
1.2 查看与指定行格式
-- 查看某张表的行格式
SHOW TABLE STATUS LIKE 't1'\G -- 看 Row_format 字段
SHOW CREATE TABLE t1\G -- 看 ROW_FORMAT 子句
-- 建表 / 改表时指定行格式
CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(100)) ROW_FORMAT=DYNAMIC;
ALTER TABLE t1 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
1.3 本系列分析的基线
上篇InnoDB 页结构详解:页类型、页头页尾、数据行与页目录-CSDN博客 分析的"额外信息 + 真实数据"结构,就是 DYNAMIC 行格式。本篇以它为主线,再看另外三种的差异。
二、DYNAMIC 行格式(默认)
2.1 总体结构
COMPACT / DYNAMIC 行格式(一条记录的物理布局)
记录起点
│
▼
┌──────────────────┬─────────────┬──────────────────┬─────────────────────────────┐
│ 变长字段长度列表 │ NULL 值位图 │ 记录头(5B/40bit) │ 真实数据(列值 + 隐藏列) │
│ (按列逆序,2B/列)│ (1bit/列) │ heap_no/next_record │ DB_TRX_ID(6) DB_ROLL_PTR(7)...│
└──────────────────┴─────────────┴──────────────────┴─────────────────────────────┘
│
└── next_record ──▶ 下一条记录的"真实数据起点"
| 部分 | 组成 | 大小 |
|---|---|---|
| 额外信息 | 变长字段长度列表 + NULL 值列表 + 记录头 | 长度列表/NULL 位图变长,记录头固定 5B |
| 真实数据 | 各用户列的值(含主键列) + 隐藏列 | 变长 |
2.2 真实数据里到底有什么
主键 DB_TRX_ID DB_ROLL_PTR 其余列
当表中存在主键时直接将对应的主键放在前面;如果不存在主键,但是存在非空唯一键,将第一个非空唯一键作为主键组织数据;如果两者都没有,则自动生成主键。
对于二级索引树,放在第一个位置的是对应的索引,数据是主键 (InnoDB下)
三、NULL 值列表的工作原理
3.1 三条规则
-
每个允许为 NULL 的列占 1 bit ,不允许为 NULL 的列不占位;
-
位图逆序存放(最后一列对应最低位);
-
若所有列都是
NOT NULL,整个 NULL 列表不占字节。示例:表 t(id INT NOT NULL, name VARCHAR(10) NULL, age INT NULL, addr VARCHAR(20) NULL)
→ 可空列按定义顺序:name、age、addr(3 列 → 位图占 3 bit,不足 1 字节按 1 字节对齐?)
位图(逆序):addr(bit2) age(bit1) name(bit0)
若 name=NULL, age=20, addr='NJ' → 位图 = 001(仅 bit0=1)
NULL 位图按字节存放,实际占用的字节数 = 向上取整(可空列数 / 8)。
3.2 为什么"自右向左"?
- 记录头是"额外信息"与"真实数据"的分界线,
next_record指向交界处; - 设计目标:向右直接读数据(按列顺序),向左读额外信息;
- 变长长度列表、NULL 位图都逆序存放,正是为了配合"向左解析"的方向。
3.3 NULL 位图里的 bit,怎么知道对应哪一列?
靠表结构元数据,不需要在行内额外存列号:
- 表结构(列定义顺序、哪些列可空、哪些是变长)保存在数据字典 中,用户表的
.ibd里还有一份 SDI 页作为表结构的 JSON 冗余副本; - 解析一行时,从数据字典拿到"可空列清单(按定义顺序)";
- 位图按逆序与可空列一一对应:第 k 个可空列对应位图倒数第 k 位(从右往左数);
- 该 bit 为 1 → 该列为 NULL,真实数据中不占空间;为 0 → 正常取值。
好处:既是空间节省(NULL 不占盘),也是语义精确(用位图区分"NULL"与值为"NULL",不会混淆)。
四、变长字段长度列表
4.1 哪些列算"变长字段"
| 类型 | 是否变长 |
|---|---|
VARCHAR / VARBINARY / TEXT / BLOB |
✅ 必为变长 |
CHAR(N)(单字节字符集,如 latin1) |
❌ 定长(按 N 字节) |
CHAR(N)(多字节字符集,如 utf8mb4) |
⚠️ 当最大字节长度可能超过 255 字节时按变长处理(utf8mb4 下即 N>63);否则仍定长 |
。
4.2 1~2 字节长度规则
变长字段长度列表逆序存放:第一个变长列的长度在列表最右端。每个字段长度占 1 或 2 字节,判定规则:
- 该列最大长度 ≤ 255 字节 → 固定用 1 字节记录(无需标志);
- 该列可能超过 255 字节 → 用 2 字节 记录,且第一个字节的最高位(MSB)置 1 作为标志位。
( InnoDB规定按照磁盘按照大端存储,但是单个字节内无论大小端都是 MSB --> LSB,作者犯蠢了 )
存储示例:某变长列实际长度 = 257 字节(0x0101,>255)
→ 2 字节表示:0000 0001 1000 0001
读取:先读第一字节(自右向左)1000 0001 ,发现最高位是 1 → 这是 2 字节表示(InnoDB规定存储方式为大端)
→ 长度 = (000 0001) << 8 | 0000 0001 = 0x101 = 257
长度 = (第一字节的低 7 位) << 8 | 第二字节的 8 位
4.3 问:2 字节能到 2¹⁵-1 ,大于 16KB (一个页的大小),一个页会不会连一行也放不下?
InnoDB 明确规定:一个页至少要能放下两行记录(否则 B+Tree 每页只有 1 条记录,树就退化成链表)。由此推出行大小约束:
16KB 页 → 行最大 ≈ 半页 ≈ 8KB(实际约 8126 字节,还要扣行开销)
对于超长字段,dynamic 的溢出处理是将数据放在溢出页中,对应字段只存放溢出页的 20 字节地址
DYNAMIC 行格式:超长字段(如 TEXT/BLOB/LONGTEXT)
┌─────────────────────────┐
│ 行内:20 字节指针 │ ──▶ 指向溢出页
│ (该列完整数据不留在行内) │
└─────────────────────────┘
溢出页 = BLOB 页(FIL_PAGE_TYPE_BLOB),数据过大时可由多个溢出页链式存放
- 特点:整列数据全部放入溢出页 ,行内只保留 20 字节的指针(指向溢出页位置);
- 这样行大小始终可控,不会因为一个大字段撑破"半页"约束;
- 溢出与否的本质判断:该列实际长度是否超过行内可容纳的阈值 ;溢出后行内表现为指针,变长长度列表记录的是行内实际保留部分的长度。
五、COMPACT 行格式:
5.1 与 DYNAMIC 的唯一区别
COMPACT 与 DYNAMIC 的额外信息、NULL 列表、记录头完全一致 ,唯一的差别在超长字段的处理:
| 行格式 | 超长字段行内保留 | 溢出部分 | 行内额外开销 |
|---|---|---|---|
| COMPACT | 前 768 字节 | 其余放入溢出页 | 20 字节指针 |
| DYNAMIC | 0 字节(全溢出) | 全部放入溢出页 | 20 字节指针 |
COMPACT:行内 = [前 768 字节数据][20 字节溢出页指针]
即"768 + 20",剩余部分在溢出页
5.2 768 字节前缀的价值
- 保留了长字段的前 768 字节 在行内,可支持前缀索引/前缀匹配(不必每次都进溢出页);
- 对"小字段混合大字段"的表,某些查询可以只读行内前缀就返回。
5.3 何时选 COMPACT
- 需要兼容老版本行为;
- 业务上对长文本有前缀查询需求;
- 默认建议仍用 DYNAMIC(官方默认),除非有明确理由。
六、COMPRESSED 行格式:DYNAMIC + 压缩
6.1 压缩原理
- 行结构与 DYNAMIC 完全相同(包括溢出处理方式);
- 额外做的事:对数据(含页)进行 zlib 压缩,减少磁盘空间占用;
- 通过
ROW_FORMAT=COMPRESSED+KEY_BLOCK_SIZE=N启用(N 为压缩页大小)。
6.2 时间换空间
| 收益 | 代价 |
|---|---|
| 磁盘占用显著下降(压缩率取决于数据重复度) | CPU 压缩/解压开销 |
| 内存中压缩页可保持压缩状态,间接省内存 | 随机访问变慢(每次读要解压) |
6.3 注意点
- 适合空间紧张、CPU 富余、以顺序扫描为主的场景(如归档表);
- 不适合写放大严重、CPU 已是瓶颈的 OLTP 核心表;
- 压缩页类型为
FIL_PAGE_COMPRESSED,与 8.0 的"表空间页压缩(COMPRESSION='zlib')"是不同机制,不要混淆。
七、REDUNDANT 行格式:历史遗留
- InnoDB 最早的格式(MySQL 4.1 时代),只为兼容旧表而存在,新表不应使用;
- 读取效率、空间利用率都不如 COMPACT 系列,官方不建议新表使用。