在 MySQL 里建表时,很少有人会主动在语句末尾加上 ROW_FORMAT,多数项目沿用默认值。但 InnoDB 的行格式并不是一个无感知参数,它决定了一条记录如何塞进 16KB 的数据页。Compact 与 Dynamic 是两种典型的行格式,前者很长一段时间是默认选项,后者在较新版本中成为默认值。两者最明显的区别发生在 TEXT、BLOB、JSON 这类大字段上:是保留一部分前缀在聚集索引页内,还是几乎全部外移到溢出页。这个选择会影响主键 B+ 树的高度、缓冲池命中率以及范围扫描效率。

一、Compact行格式的设计与局限
Compact 的名称容易让人误解为一定节省空间。事实上,它的核心特征是当一行数据超过数据页可用空间时,将超长字段的一部分内容留在页内,其余写入溢出页。对于 TEXT 或 BLOB 列,Compact 会在聚集索引页中保留前 768 字节的数据,同时用一个 20 字节的指针指向溢出页地址。这样设计的好处是,如果查询只读取前 768 字节以内的内容,例如 LEFT(content, 100),有机会避免访问溢出页。但缺点也很直接:即使查询只需要主键,这一行仍要占住 768 字节的前缀空间。
可以想象一个内容型表,平均每条记录的 content 长度为 10KB。使用 Compact 时,聚集索引页中每条记录至少携带 768 字节的 TEXT 前缀,加上其他列和记录头,单条主键记录很容易超过 900 字节。一个 16KB 的页除去页头、页尾和目录槽,实际可用空间约 15KB,也就只能放下十几条记录。主键 B+ 树的非叶子节点可能变高,范围扫描时需要加载更多页。更麻烦的是,更新大字段时如果前缀部分长度发生变化,会触发页内记录移动,容易留下碎片。
CREATE TABLE article_compact (
id BIGINT NOT NULL AUTO_INCREMENT,
title VARCHAR(200),
content TEXT,
created_at DATETIME,
PRIMARY KEY (id)
) ENGINE=InnoDB ROW_FORMAT=COMPACT;
INSERT INTO article_compact (title, content, created_at)
VALUES ('Compact行格式示例', REPEAT('A', 10240), NOW());
上面这行 10KB 的 content,在 Compact 行格式下会被拆成两部分:前 768 字节留在聚簇索引叶节点,其余约 9.3KB 写入独立的溢出页。查询 SELECT id FROM article_compact WHERE id = 1 虽然不需要 content,但主键页仍然被 768 字节前缀拖累。如果表里存在大量类似记录,主键索引占用的空间会明显高于预期。
二、Dynamic行格式的溢出改进
Dynamic 行格式与 Compact 最大的不同在于大字段的溢出策略。Dynamic 不再保留 768 字节的前缀,而是只在聚集索引页中存放一个 20 字节的指针,实际数据整体写入溢出页。对于前面 10KB 的 content 字段,Dynamic 在页内只占 20 字节,单页能容纳的主键记录数成倍增加。这种策略非常适合内容平台、日志存储、评论系统等 TEXT/BLOB 占比高的场景。
需要区分的是,Dynamic 并不是对所有变长字段都一律外移。像 VARCHAR(200) 这类较短的列,只要整行不超过页面限制,仍然会把数据存在页内。只有当整行太大导致页面放不下时,InnoDB 才会选择最长的列进行溢出。Dynamic 还配套使用 Barracuda 文件格式,支持更现代的溢出页链表管理,某些情况下可以原地更新溢出页,减少旧页空间浪费。
CREATE TABLE article_dynamic ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(200), content TEXT, created_at DATETIME, PRIMARY KEY (id) ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; SELECT NAME, ROW_FORMAT FROM information_schema.INNODB_TABLES WHERE NAME LIKE 'blog/article_dynamic';
执行上述建表语句后,再插入同样 10KB 的数据,可以发现 article_dynamic 的主键页里每条记录只保留一个 20 字节指针。对只访问主键和短列的范围扫描来说,单页内能缓存的记录更多,扫描同样主键范围需要读取的页变少。当然,一旦查询必须返回完整 content,Dynamic 仍要访问溢出页,但这一部分 IO 与 Compact 读取溢出页的代价相当,而且主键索引本身的瘦身会带来持续收益。
三、索引体积、范围扫描与碎片率对比
从索引体积看,Dynamic 在含大字段的表上通常更占优势。以 100 万行、每条 content 平均 10KB 的数据量估算,Compact 的聚集索引叶节点每条记录多出约 768 字节,累计约 732MB 主键页空间;Dynamic 则只多出 20 字节,约 19MB。这个估算未包含列数据本身的其他部分,但量级差异已经足够说明问题。主键索引体积缩小后,B+ 树高度更容易维持在三层以内,对点查和范围查询都有帮助。
范围扫描是两者差异最容易被感知的场景。执行 SELECT id, title FROM article WHERE id BETWEEN 10000 AND 20000 时,如果使用 Dynamic,主键页可以容纳更多目标行,缓冲池命中率提高;而 Compact 的主键页被 TEXT 前缀撑大,同样的 buffer pool 只能缓存更少页。不过如果业务 SQL 总是 SELECT * 并且必须返回大字段,两种行格式都会触发溢出页访问,差异更多体现在主键索引元数据和页分裂频率上。
| 对比项 | Compact | Dynamic |
|---|---|---|
| 大字段页内保留 | 前768字节 | 20字节指针 |
| 主键索引体积 | 较大 | 较小 |
| 范围扫描页读取 | 更多 | 更少 |
| 读取完整大字段 | 需访问溢出页 | 需访问溢出页 |
| 更新大字段碎片 | 页内前缀变化易产生碎片 | 主键页基本不变,碎片更低 |
碎片率方面,Compact 在更新大字段导致前 768 字节长度改变时,可能引起页内多版本数据和空隙增多。Dynamic 因为主键页只有指针,大字段更新发生在溢出页,聚集索引页结构更稳定。但这也带来一个细节:如果 TEXT 内容通常小于 768 字节,例如平均只有 500 字节,Compact 可以直接在页内读取完整内容,Dynamic 反而可能因为整行未超出页面限制而仍将数据放在页内,此时二者差异不大。因此是否转换要结合字段长度分布,不能只看行格式名称。
四、如何安全迁移到Dynamic
在决定是否把存量表从 Compact 迁移到 Dynamic 前,可以先统计大字段的平均长度和最大长度。通过 SELECT AVG(LENGTH(content)), MAX(LENGTH(content)) FROM article_compact 这类语句可以得到数据分布。如果平均长度超过 768 字节且查询模式以主键范围扫描为主,迁移收益会比较明显;如果大字段平均只有几百字节,或者业务频繁读取完整内容且查询条件无法有效利用主键,收益则会打折扣。
迁移操作本身会重建表,对大表需要选择低峰期,并使用在线 DDL 工具降低锁影响。普通的 ALTER TABLE article_compact ROW_FORMAT=DYNAMIC; 在 InnoDB 中可以执行在线操作,但重建期间仍会产生额外磁盘和 CPU 开销。完成后建议执行 OPTIMIZE TABLE article_compact; 回收旧表空间。也可以用 SHOW TABLE STATUS LIKE 'article_compact'; 查看行格式和平均行长,或在操作系统层确认 ibd 文件大小变化,Windows 环境可查看 C:\MySQL\data\blog\article_compact.ibd。
-- 查看当前行格式 SHOW TABLE STATUS FROM blog LIKE 'article_compact'; -- 在线重建为Dynamic ALTER TABLE article_compact ROW_FORMAT=DYNAMIC; -- 回收空间并更新统计信息 OPTIMIZE TABLE article_compact;
需要注意的是,Dynamic 依赖 Barracuda 文件格式和独立表空间。如果数据库是从非常老的版本升级而来,可能仍存在 Antelope 文件格式,需要先检查 innodb_file_format 和 innodb_file_per_table 参数。新安装的 MySQL 实例通常已默认开启这些选项,无需额外处理。历史上已经使用 Compact 运行多年的表,也不建议仅因为看到新默认值就大规模转换,最好结合索引扫描、页读取和表空间大小变化做小范围测试,再推广到核心表。