Skip to content

存储结构

Quote

参考自 小林 Coding MySQL 一行记录是怎么存储的?

存储的行为是由存储引擎实现的,不同的存储引擎实现不同,这里只讲 InnoDB。

默认情况下, MySQL 的数据库文件存放在 /var/lib/mysql 目录下,每个数据库对应一个目录,目录名称就是数据库名称。

目录下存放着数据库的表文件,表文件的后缀名是 .frm,存放表的结构信息。表的数据存放在 .ibd 文件中。

Example
1
2
3
4
5
$ tree /var/lib/mysql/user_center
user_center
    ├── db.opt
    ├── userinfo.frm
    └── userinfo.ibd

以上是 user_center 数据库下的 userinfo 表:

  • db.opt 存放数据库的一些配置信息,例如数据库的默认字符集和字符校验规则等等。
  • userinfo.frm 存放表的结构信息,MySQL 中每建立一张表都会生成一个 .frm 文件,主要包含表结构定义等信息。
  • userinfo.ibd 存放表的数。表数据既可以存放在共享表空间(文件名: ibdata1)中,也可以存放在独立的表空间中(文件名: 表名.ibd), 这个由 innodb_file_per_table 参数决定,1 则使用独占表空间,0 则使用共享表空间。从 MySQL 5.6.6 开始,innodb_file_per_table 默认为 1。

除了这些文件,还有一些其他文件,例如 ibdata1ib_logfile0ib_logfile1ib_logfile2 等等,这些文件是 InnoDB 存储引擎的文件,用来存放 InnoDB 存储引擎的数据和日志信息。

表空间文件结构

一共有这些单位:表空间(tablespace)、段(Segment)、区(Extent)、页(Page)、行(Row)

表空间(tablespace)由段(Segment)构成,段由区(Extent)构成,区由页(Page)构成,页由行(Row)构成,页是存储的最小单位。

表结构

表结构

行 row

数据库表中的记录都是按行存放的,每行记录根据不同的行格式有不同的存储结构。

页 page

记录是按照行存储的,但是数据库不会以「行」为单位,否则一次读取(一次 I/O)只能处理一行的数据,效率很低。

所以 InnoDB 的数据是按照「页」为单位来读取的,即当我们要读一条记录的时候,这一页的数据都将被读到内存中,默认每个页的大小为 16KB

页是 InnoDB 磁盘管理的最小单元,所以每次读写都是以 16KB 为单位,一次最少读 16KB 的内容到内存,一次最少把内存中的 16KB 内容刷新到磁盘。

页的类型有很多,常见的有 数据页、UNDO 日志页、溢出页 等。表中的行记录是存储在「数据页」的。

Tip
  • 数据页(Data Page): 存储表中的行记录。
  • UNDO日志页(Undo Log Page): 记录事务执行前的数据,用于事务回滚。
  • 溢出页(Overflow Page): 用于存储大字段数据,当一个行的数据大小超过一个页的大小时,会把超出的部分存储到溢出页中。

区 extent

InnoDB 存储引擎是用 B+树来组织数据的。B+树中的每一层都是通过双向链表连接起来的。

b+tree

如果以页为单位来分配空间,那么相邻的两个页之间的物理位置不一定是连续的,会导致磁盘查询时有大量的随机 IO,效率很低。

所以 InnoDB 会把连续的页组织成一个区,这样可以减少磁盘的随机 IO,提高查询效率。

在表中数据量大的时候,为某个索引分配空间的时候就不在按照页(Page)为单位分配了,而是按照区(Extent)为单位分配,一个区包含多个页。

每个区的大小默认为 1MB,对于 16KB 的页,一个区就包含 64 个页。

段 segment

段是由区组成的,一个段可以包含多个区,段是 InnoDB 存储引擎管理空间的逻辑单位。 一般分为 数据段、索引段、回滚段

Tip
  • 数据段(Data Segment): 存放 B + 树的叶子节点的区的集合。
  • 索引段(Index Segment): 存放 B + 树的非叶子节点的区的集合。
  • 回滚段(Rollback Segment): 存放的是回滚数据的区的集合,用于事务回滚。

小结

Note

按行存储、按页读取、按区分配、按段分类,这就是 InnoDB 存储引擎的存储结构。

InnoDB 行格式

行格式 Row Format,就是一条记录的存储结构。

InnoDB 提供了 4 种行格式,分别是:

  • Redundant 不是紧凑的
  • Compact 紧凑的
  • Dynamic 动态的
  • Compressed 压缩的

Dynamic 和 Compressed 这两个和 Compact 一样,都是紧凑的,只是存储的方式不同。

MySQL 5.7 之前的版本默认的行格式是 Compact,MySQL 5.7 之后默认的行格式是 Dynamic

Compact 行格式

Compact 行格式分为前半部分:这一行的额外信息,后半部分:这一行的实际数据

额外信息部分是为了描述这条记录而添加的一些信息,包括:

  1. 变长字段的长度列表
  2. NULL 值的位图
  3. 记录头信息

变长字段的长度列表

MySQL 支持一些变长的类型,如 varchar(n)、text、blob 等。这些变长类型占用两部分存储空间:真正的数据内容长度(占用的字节数)

这个长度就记录在额外信息的变长字段的长度列表中。每一行的 varchar 字段都可能是不一样的,所以每一行的 varchar 字段的长度都不一样。

Example

1
2
3
4
5
6
7
8
CREATE TABLE t (
    id          INT(11) PRIMARY KEY,
    name        VARCHAR(10)  DEFAULT NULL,
    phone       CHAR(11)     DEFAULT NULL,
    description VARCHAR(100) DEFAULT NULL
) 
    ENGINE = InnoDB
    DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;
上面的表中,namedescription 字段都是 varchar 类型的,所以每一行的这两个字段的长度都是不固定的,需要额外的空间来存储这两个字段的长度。 (为了方便理解,这里用 ascii 字符集)

现在插入几条数据:

1
2
3
4
5
6
INSERT INTO t
    (id, name, description) 
VALUES 
    (1, 'a', '13800000000', 'xyz'),
    (2, 'ab', '13900000000', NULL),
    (3, NULL, '13700000000',NULL);

id name phone description
1 a 13800000000 xyz
2 ab 13900000000 NULL
3 NULL 13700000000 NULL
  • 第一条记录:

    name 的值为 a,长度 1 字节,十六进制是 0x01;

    description 的值为 xyz,长度 3 字节,十六进制是 0x03;

    列 id 和 phone 为固定长度,这里不用管。

    所以这一列的长度列表为 「0x03, 0x01」

  • 第二条记录:

    name 的值为 ab,长度 2 字节,十六进制是 0x02;

    description 的值为 NULL,NULL 是不会存放在真实数据里的,长度列表中不需要保存这个字段的长度。

    这一列的长度列表为 「0x02」

  • 第三条记录:

    name 和列 description 的值为 NULL。

    这一列的长度列表为空 「」

row_format

NULL 值的位图

表中的字段可以为 NULL,而 NULL 不占用存储空间,只会使用一个位来表示对应的字段是否为 NULL。 这个表示的东西叫做 NULL值位图(NULL Bitmap)

假设表中有 4 个字段允许为 NULL,其中第 3 个字段为 NULL,那么 NULL 值位图就是 0100,表示第 3 个字段为 NULL,其他字段不为 NULL。

注意位图跟字段的顺序是相反的,第一个字段对应的是位图的最后一个位。 这么做的原因是为了方便处理,因为位图是变长的,不是固定长度的。

null_bitmap


不足的位图会用 0 补齐。如果允许为 NULL 的字段个数在 8 个以内,那么 NULL 值位图的长度是 1 字节,如果超过 8 个,那么长度是 2 字节,以此类推。

具体可以参考上面例子中的示意图。

记录头信息

记录头信息大小固定位 5 字节,共 40 位,不同位的含义如下:

名称 大小(bit) 位置 描述 详细描述
预留位 1 1 0-1 保留位,未使用
预留位 2 1 1-2 保留位,未使用
delete_mask 1 2-3 删除标记,1 表示删除,0 表示未删除 删除时并不是真的物理删除,而是将这个位标记为 1
min_rec_mask 1 3-4 最小记录标记,1 表示最小记录,0 表示不是最小记录 标记该记录是否为B+树的非叶子节点中的最小记录
n_owned 4 4-8 记录的引用计数 记录被多少个事务引用
heap_no 13 8-21 记录的堆编号 记录在页中的位置
record_type 3 21-24 记录的类型:0普通记录、1B+树非叶节点记录、2最小记录、3最大记录
next_record 16 24-40 下一条记录的偏移量 下一条记录的偏移量,用于记录的链表

记录的真实数据

记录的真实数据就是表中的字段的值,这部分数据是紧凑的存储在一起的。

不过除了真实数据还有 3 个隐藏字段:

列名 大小(byte) 描述
row_id 6 行的 ID,当表没有指定主键也没有唯一约束时,InnoDB 会启用这个隐藏ID
trx_id 6 事务 ID,表示这个数据是由哪个事务生成的
roll_pointer 7 回滚指针,这条记录上一个版本的指针

trx_id 和 roll_pointer 是用来支持 MVCC 的,MVCC 是 InnoDB 的一个特性,用来支持事务的并发控制。