存储结构¶
Quote
参考自 小林 Coding MySQL 一行记录是怎么存储的?
存储的行为是由存储引擎实现的,不同的存储引擎实现不同,这里只讲 InnoDB。
默认情况下, MySQL 的数据库文件存放在 /var/lib/mysql
目录下,每个数据库对应一个目录,目录名称就是数据库名称。
目录下存放着数据库的表文件,表文件的后缀名是 .frm
,存放表的结构信息。表的数据存放在 .ibd
文件中。
Example
以上是 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。
除了这些文件,还有一些其他文件,例如 ibdata1
、ib_logfile0
、ib_logfile1
、ib_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+树中的每一层都是通过双向链表连接起来的。
如果以页为单位来分配空间,那么相邻的两个页之间的物理位置不一定是连续的,会导致磁盘查询时有大量的随机 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 行格式分为前半部分:这一行的额外信息
,后半部分:这一行的实际数据
。
额外信息部分是为了描述这条记录而添加的一些信息,包括:
- 变长字段的长度列表
- NULL 值的位图
- 记录头信息
变长字段的长度列表¶
MySQL 支持一些变长的类型,如 varchar(n)、text、blob 等。这些变长类型占用两部分存储空间:真正的数据内容 和 长度(占用的字节数)。
这个长度就记录在额外信息的变长字段的长度列表中。每一行的 varchar 字段都可能是不一样的,所以每一行的 varchar 字段的长度都不一样。
Example
name
和 description
字段都是 varchar 类型的,所以每一行的这两个字段的长度都是不固定的,需要额外的空间来存储这两个字段的长度。
(为了方便理解,这里用 ascii 字符集)
现在插入几条数据:
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。这一列的长度列表为空 「」。
NULL 值的位图¶
表中的字段可以为 NULL,而 NULL 不占用存储空间,只会使用一个位来表示对应的字段是否为 NULL。 这个表示的东西叫做 NULL值位图(NULL Bitmap)
假设表中有 4 个字段允许为 NULL,其中第 3 个字段为 NULL,那么 NULL 值位图就是 0100,表示第 3 个字段为 NULL,其他字段不为 NULL。
注意位图跟字段的顺序是相反的,第一个字段对应的是位图的最后一个位。 这么做的原因是为了方便处理,因为位图是变长的,不是固定长度的。
具体可以参考上面例子中的示意图。
记录头信息¶
记录头信息大小固定位 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 的一个特性,用来支持事务的并发控制。