开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第02天,点击查看活动详情
真实数据在不同的“数据引擎”中存储的格式一般是不一样的。 InnoDB 中若干个数据记录组成一个数据页,并且以数据页作为内存、磁盘之间交互的基本单位。 InnoDB 中默认的数据页大小为16KB。
数据页中记录存放在磁盘上的格式被称为是行格式、或记录格式。 InnoDB 中行格式分为四种:
- Compact
- Redundant
- Dynamic
- Compressed
如何指定行格式?
CREATE / ALTER 语句中通过 ROW_FORMAT=Compact|Redundant|Dynamic|Compressed 指定。
例如,如下语句:
--- 创建表格
CREATE TABLE `record_format_demo` (
`c1` varchar(10) DEFAULT NULL,
`c2` varchar(10) NOT NULL,
`c3` char(10) DEFAULT NULL,
`c4` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;
--- 插入数据
INSERT INTO record_format_demo(c1, c2, c3, c4)
VALUES ('aaaa', 'bbb', 'cc', 'd'),
('eeee', 'fff', NULL, NULL);
--- 数据内容
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
接下来,我们主要学习一下 Compact 类型的行格式。 其他格式可以依葫芦画瓢的学习、掌握。
01-Compact 行格式概览
Compact 行格式下,每行内容主要包括两部分内容,记录的额外信息(元信息)和记录的真实数据:
- 元信息,指描述记录本身的一些信息。包括三类:
- 变长字段 长度列表
- NULL 值列表
- 记录头信息
- 数据,真实数据,包括定义的列、InnoDB 自动插入的列。
02-元信息
02.1-变长字段长度列表
MySQL 支持变长字段,例如 VARCHAR、VARBINARY、各类 TXT、BLOB 类型等。 所以,存储引擎需要记录这些变长字段的实际内容,以及占用的字节数,以便能够正确的解析。
元信息中的变长字段长度列表,就是由所有变长字段实际占用的字节数逆序存储构成的。 例如,record_format_demo 表中的 c1\c2\c4 列均为 VARCHAR 类型变长字段。
| aaaa | bbb | cc | d |
对于第一条记录,c1 占用字节为04,c2 占用字节为03,c4 占用字节为01,则该条记录在 InnoDB 中以 Compact 格式存储时, 元信息中的变长字段长度列表为:01 03 04(逆序)
对于此变长字段长度列表占用的数据大小有如下的计算规则:变长字段数 NC * 表示每个变长字段占用长度所需的空间(1字节或2字节) U。 对于前者 NC 很容易得出。 后者 U 的计算规则稍微复杂点,需要先介绍下 W、M、L 的含义:
- 对于某个字符集 charset,表示一个符号可能需要的字节数不尽相同,例如 utf8 中是3字节,gbk 中是2字节,ascii 中是1字节。 此值记作 W。
- 对于变长类型来说,例如 VARCHAR(M) 表示最多存储 M 个字符。
- 根据上述两个值,某个变长字段的最大长度为 W * M,假设它实际占用的长度为 L < W * M
有了上述定义,U 的计算规则如下:
- M * W <= 255(1个字节能表示的最大数字),那么 U = 1
- M * W > 255时,分为两中情况:
- L <= 127,U = 1
- L > 127,U = 2
| eeee | fff | NULL | NULL |
变长字段长度列表中不存储值为 NULL 的字段,对于第二条记录只存储 03 04(逆序)。
02.2-NULL 值列表
通过位向量方式存储 NULL 值。
- 首先,收集表格中所有可以为空的列,例如 record_format_demo 中可空列为 c1\c3\c4
- 用合式的字节来表示 NULL 值,以逆序的方式,每个比特位对应一个可空列,且比特位为1时表示 NULL,为0时表示非 NULL 值。
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
例如,对于第二条记录: NULL 值列表为:0000 0110(逆序,对应 c4\c3\c1) 对于第一条记录: NULL 值列表为:0000 0000
02.3-记录头信息
固定长度,5个字节组成,即40个比特位。
- 0-1,预留位,未使用
- 2,delete_mask,标记该记录是否被删除
- 3,mini_rec_mask,B+树的每层非叶子节点中的最小记录都会添加该标记
- 4-7,n_owned,表示当前记录拥有的记录数
- 8-20,heap_no,表示当前记录在记录堆的位置信息
- 21-23,record_type,表示当前记录的类型
- 0 表示普通记录
- 1 表示B+树非叶子节点记录
- 2 表示最小记录
- 3 表示最大记录
- 24-39,next_record,表示下一条记录的相对位置
03-行数据
前面几节都在介绍元信息中包含的数据。 本节介绍的是记录的真实数据,包括表中定义的列、InnoDB 自动插入的列。
03.1-InnoDB 自动插入的列
真实数据其实就是行记录中每列的具体值。 除了表中定义的字段外,MySQL 会为每个记录增加如下列(也称为隐藏列):
- DB_ROW_ID(optional),6字节,行 ID,唯一标识一条记录;
- DB_TRX_ID,6字节,事务 ID
- DB_ROLL_PTR,7字节,回滚指针
注:InnoDB 对主键的生成策略是:优先使用用户定义的主键; 如果没有定义主键,则选取一个声明为 Unique 的列作为主键; 如果没有任何 Unique 列,则插入 DB_ROW_ID,并以其作为主键。
03.2-CHAR(X) 与 VARCHAR(Y) 的存储格式比较
根据前面章节的介绍,VARCHAR(Y) 类型的字段根据是否为空,可以分为如下几种情况:
- 若值为空,则在 NULL 值列表对应的比特位处的值为1,在变长字段长度列表、真实数据处变不再存储;
- 若值非空,则在变长字段长度列表中记录其实际长度,在真实数据处存储具体数据内容。
CHAR(X) 类型根据使用字符集的不同,占用的空间和存储方式也略有区别:
- 当使用定长字符集(例如 ascii,每个字符都由1个字节表示),字段长度不会表示在变长字段长度列表,只在真实数据中存储实际值。 当实际存储内容不足 X 字符时,也会占用 X 字符的长度,其余空闲的会以0补齐。
- 当使用不定长字符集(例如 utf-8,每个字符由1-3个字节表示),字段长度会表示在变长字段长度列表中,真实数据中存储其实际值。 此种情形下,CHAR(X) 要求真实数据至少占用 X 字节,即使只存储一个空字符串,也会占用 X 字节。
03.3-行溢出
InnoDB 中记录是存储在页中的,而且内存与磁盘之间的数据交换也是以页为单位的。 页大小为16KB,即16384字节。 当某一行的大小超过页能存储的大小(并不是16384,因为有元信息的存在,页实际存储数据的部分小于16384),就会出现一条记录存储在多个页中的情况,此时称为行溢出。 在 Compact 行格式下,行溢出时,行数据这部分内容只会存储一部分内容(768字节),然后用20字节指向其他页面(称为溢出页)。