MySQL「02」InnoDB 记录结构

196 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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 的计算规则如下:

  1. M * W <= 255(1个字节能表示的最大数字),那么 U = 1
  2. M * W > 255时,分为两中情况:
    • L <= 127,U = 1
    • L > 127,U = 2
| eeee | fff | NULL | NULL |

变长字段长度列表中不存储值为 NULL 的字段,对于第二条记录只存储 03 04(逆序)。

02.2-NULL 值列表

通过位向量方式存储 NULL 值。

  1. 首先,收集表格中所有可以为空的列,例如 record_format_demo 中可空列为 c1\c3\c4
  2. 用合式的字节来表示 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) 类型的字段根据是否为空,可以分为如下几种情况:

  1. 若值为空,则在 NULL 值列表对应的比特位处的值为1,在变长字段长度列表、真实数据处变不再存储;
  2. 若值非空,则在变长字段长度列表中记录其实际长度,在真实数据处存储具体数据内容。

CHAR(X) 类型根据使用字符集的不同,占用的空间和存储方式也略有区别:

  1. 当使用定长字符集(例如 ascii,每个字符都由1个字节表示),字段长度不会表示在变长字段长度列表,只在真实数据中存储实际值。 当实际存储内容不足 X 字符时,也会占用 X 字符的长度,其余空闲的会以0补齐。
  2. 当使用不定长字符集(例如 utf-8,每个字符由1-3个字节表示),字段长度会表示在变长字段长度列表中,真实数据中存储其实际值。 此种情形下,CHAR(X) 要求真实数据至少占用 X 字节,即使只存储一个空字符串,也会占用 X 字节。

03.3-行溢出

InnoDB 中记录是存储在页中的,而且内存与磁盘之间的数据交换也是以页为单位的。 页大小为16KB,即16384字节。 当某一行的大小超过页能存储的大小(并不是16384,因为有元信息的存在,页实际存储数据的部分小于16384),就会出现一条记录存储在多个页中的情况,此时称为行溢出。 在 Compact 行格式下,行溢出时,行数据这部分内容只会存储一部分内容(768字节),然后用20字节指向其他页面(称为溢出页)。