更多博客请见 我的语雀知识库
小林coding那边已经很详细了,这里我只对一些比较重要的点和不太懂的地方做出补充
小林coding. (n.d.). MySQL 一行记录是怎么存储的?. Retrieved from MySQL 一行记录是怎么存储的?

表空间结构

image.png

  • 行, 自然对应数据表中的每一行数据
  • 页, 有限行的集合。查询的时候肯定不是一行数据一行数据读取,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。而是读取一批的行数据。存储这一批行数据的结构叫做页
  • 区, 固然每次可以批量读取页数据,但如果每一页在磁盘不连续,那么就会出现大量随机磁盘IO。为了提升性能,把一批的页顺序存储在一个结构里,叫做区。
  • 段, 在B+树存储引擎里,分有索引段、数据段和回滚段三部分。索引段用于存储非叶子节点,数据段存储叶子节点。区在叶子节点中通过双向链表连接。而回滚段就和MVCC有关了,为事务隔离性提供了支持

行结构

InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。 这里只介绍Compact行格式。 COMPACT.drawio.jpg

null值列表

null值列表都是以16进制数表示的 null值列表转换为二进制后每一位代表对应的列(逆序对应,如下图) image.png

变长字段长度列表

变长字段(varchar)长度列表和null值列表都是以16进制数表示的 但是与null列表不同,这里展开为二进制没有任何意义,因为保存的是某个varchar字段占据的字节数大小。其中每个16进制数据倒序对应于每个varchar字段,参考上图

具体结构示意图

假设有表数据如下:

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL,
  `name` VARCHAR(20) DEFAULT NULL,
  `phone` VARCHAR(20) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

image.png 选取有代表性的第二条记录:

  • name 列的值为 bb,真实数据占用的字节数是2字节,十六进制 0x02
  • phone 列的值为 1234,真实数据占用的字节数是 4 字节,十六进制 0x04
  • age 列和 id 列不是变长字段,所以这里不用管。

image.png 由于逆序对应, 变长字段长度列表中,04代表phone字段在这一行占据了4个字节,02代表name字段在这一行占据了2个字节。 null值列表中,16进制0X04展开为二进制是100,第三位的1代表对应的最后一个字段age是null,第二位的0代表对应的phone字段不为null;第一位的0代表对应的name字段不为null。 注意,NULL 值并不会存储在行格式中的真实数据部分。也就是说第二行的真实数据部分只有列一和列二,列三不存在。

每个数据库表的行格式都有「NULL 值列表」吗?

NULL 值列表也不是必须的。 当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。 所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以至少节省 1 字节的空间( 因为不满8位会补0凑齐所以NULL 值列表至少占用 1 字节空间,)。

为什么是逆序存放?

image.png

img.png
如上图,逆序存放可以让使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。 如橙色的name列可以和它对应的记录“0”靠的更近

行数据溢出

页的大小默认是16KB,16384字节。如果像varchar、 TEXT、BLOB这样不定长度的字段 非常大,导致一页都存不下一行数据,那么就会发生行溢出的情况 这是本页只会存储这一行的一部分数据,剩下的放在另一页里,两页通过指针连接 image.png