Skip to content

存储结构

存储的行为是由存储引擎实现的,不同的存储引擎实现不同,这里只讲 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。

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