从一行记录理解 MySQL 的存储设计
先理解页为什么存在
SQL 里看到的是一行行记录,InnoDB 管理数据时,一个重要单位是页,默认大小为 16KB。页向上组成区和段,再由表空间管理。默认页大小下,一个区包含 64 个连续页,共 1MB;按区分配空间,有助于让数据在磁盘上的位置更连续。
我觉得这里最有用的理解是:查一行数据,要考虑它所在的页。页已经在内存缓存里,就可能直接读取;需要访问磁盘时,读取的也包含页里的其他内容。因此,在其他条件相近时,一行占用越少,同一个页就能容纳更多记录,同样大小的缓存也能装下更多数据。
字段大小和查询成本的联系,就从这里开始。比如一张文章表,同时保存标题和很长的正文,列表页却只需要标题。理解它们实际存放在哪里,才能判断哪些数据会占用常用页里的空间。
一行数据需要说明自己怎么读
把多个字段的值连续放在一起,还需要让数据库知道各个字段的边界。COMPACT 行格式可以粗略画成这样:
1 | 变长字段长度列表 | NULL 位图 | 记录头 | 系统字段和用户字段 |
变长字段长度列表记录非 NULL 的变长字段实际占用多少字节,数据库据此解析字段。固定长度可以从表结构确定的字段,就不用在这里再记一次长度。NULL 位图则用二进制位标记允许为空的列:每列一位,按整字节分配。9 个允许为空的列需要 2 字节,即使这一行的 9 列都有值,位图仍然存在。值为 NULL 的列不再占用字段数据空间。这里的开销取决于允许为空的列数。
记录头还保存删除标记、记录类型,以及页内下一条记录的位置。这样,页里的记录就能被组织和遍历。
一行里也有支持事务的系统字段。聚簇索引记录保存事务 ID 和回滚指针,后者指向 undo 日志,数据库可以用它重建旧版本。删除记录时,也会先做标记,等旧版本不再被需要,再由 purge 清理。这让我更容易理解,支持事务需要在数据旁边保存额外的信息。
varchar 的长度有两种含义
VARCHAR(100) 的 100 表示字符数,实际存储要看字节数。使用 utf8mb4 时,一个字符最多需要 4 字节,所以 100 个字符最多需要 400 字节的数据空间;如果只写入 abc,数据部分就是 3 字节,再加长度等开销,不会预先填满 400 字节。这也解释了为什么 varchar 的最大长度不能只记一个数字。
MySQL 的行大小限制为 65,535 字节,其中也包含相应的存储开销。如果表里只有一个允许为 NULL 的 varchar 字段,且字符集为 ASCII,那么最大字符数可以这样算:
1 | 65,535 - 2 字节长度信息 - 1 字节 NULL 信息 = 65,532 |
太长的字段可以放到页外
前面的限制讨论的是整行的大小,InnoDB 还要考虑记录在页内占用多少空间。默认 16KB 页下,页内记录的大小上限略小于 8KB,所以长字段可能需要移到页外。
行溢出的处理方式,是把选中的长字段内容放到额外的溢出页,在原记录里留下定位信息。不同的行格式,对留在原页里的内容有不同选择:
| 行格式 | 被选中存到页外的字段,在原记录里保留什么 |
|---|---|
| REDUNDANT、COMPACT | 前 768 字节,以及 20 字节的页外引用 |
| DYNAMIC、COMPRESSED | 20 字节的页外引用,字段内容放到溢出页 |
这里说的是已经被选中做页外存储的字段。短字段仍然可以留在原页里;是否需要移出去,要结合整行大小和页大小判断。MySQL 8.4 默认使用 DYNAMIC。
按这个结构理解,长内容放到页外可以让原来的页容纳更多记录,但读取完整内容时,就可能需要继续访问溢出页。省下常用页里的空间,也增加了取得长内容的路径。这是一个和访问方式有关的取舍。
回到文章表的例子,我会先看列表页是否只查标题,详情页是否才需要正文,再判断有没有必要把正文拆出去。DYNAMIC 已经能把长字段放到页外,拆表仍然要考虑查询方式和维护成本,不能只看字段长不长。
同样,是否设置 NOT NULL,首先要看业务上是否允许缺失。用空字符串代替“未知”,可能省了一点位图空间,却改变了数据的含义。主键也要结合存储考虑:InnoDB 的二级索引记录包含主键,主键越长,二级索引通常也越占空间。
对我来说,读到这里,再看一张表就有了几个具体的问题:每行实际有多大,常用查询需要哪些字段,这些字段会留在同一页里吗,读完整数据还要去哪里找。先沿着这些问题想,数据库设计里的那些建议才比较容易理解。
- 标题: 从一行记录理解 MySQL 的存储设计
- 作者: Hyacehila
- 创建于 : 2026-10-02 00:00:00
- 链接: https://hyacehila.github.io//blog/2026/10/02/mysql-row-storage-design/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。