InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT

一、行格式总览

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 三条规则

  1. 每个允许为 NULL 的列占 1 bit ,不允许为 NULL 的列不占位;

  2. 位图逆序存放(最后一列对应最低位);

  3. 若所有列都是 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,怎么知道对应哪一列?

靠表结构元数据,不需要在行内额外存列号:

  1. 表结构(列定义顺序、哪些列可空、哪些是变长)保存在数据字典 中,用户表的 .ibd 里还有一份 SDI 页作为表结构的 JSON 冗余副本;
  2. 解析一行时,从数据字典拿到"可空列清单(按定义顺序)";
  3. 位图按逆序与可空列一一对应:第 k 个可空列对应位图倒数第 k 位(从右往左数);
  4. 该 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 字节,判定规则:

  1. 该列最大长度 ≤ 255 字节 → 固定用 1 字节记录(无需标志);
  2. 该列可能超过 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 系列,官方不建议新表使用。
相关推荐
春涧草茶23 分钟前
慢就是快16-1协程
开发语言·前端·python
liangbo739 分钟前
Spring系列03:Bean 的三种注册方式
java·spring
余槐i43 分钟前
MySQL 8.0 联合索引走错:为什么加了索引查询反而从 200ms 变成 8s
java·spring boot·mysql·docker·性能优化
SL_staff44 分钟前
制造业订单交付总在最后一刻爆雷?用结构化计划模板打通跨系统协同断点
java·开源·全栈
字节跳动开源1 小时前
OpenViking连接业务系统,LLM Wiki一键编译
数据库·开源
Dovis(誓平步青云)1 小时前
启元 T1 Pro 人机相声搭档:语音-触摸双通道的启元机器人节奏引擎设计
开发语言·前端·javascript·microsoft·交互
做科研的周师兄1 小时前
【R语言实战】MixSIAR 氢氧同位素水源解析全流程:从混合空间校验到贝叶斯 MCMC 收敛诊断(后附完整代码)
java·大数据·开发语言
有点小困9771 小时前
Spring Boot 异步任务 ClassLoader 踩坑实录:ClassNotFoundException 的诡异真相
java
aiks_yuan1 小时前
Java 正则表达式很慢怎么办:Pattern 预编译只快 2~5 倍,真正卡死线程的是回溯(JDK 8、11、17 实测)
java