# 前言

MySQL 服务器中负责对表中数据进行读取和写入工作的部分是存储引擎,而服务器支持不同类型的存储引擎,如 InnoDB,MyISAM,MEMORY 等。真实数据在不同存储引擎的存放格式可能不同。因为 InnoDB 现在是 MySQL 默认的存储引擎,所以会介绍其相关特性。

在 MySQL 中,表中的一行数据被称为一条记录,记录保存在页中,页组成 B + 树,通过二分查找迅速找到记录。如果你还看不太懂,也没太大关系,这句话只是想告诉你 InnoDB 存储数据的本质。

# InnoDB 页

数据库会频繁发生数据的读取和写入,这就像操作系统的文件系统一样,会发生大量的与磁盘的交互。但是从磁盘中直接读写数据非常慢,也就是说数据库不可能每次读写数据都直接和磁盘交互,所以就将数据分为若干个页,以页作为磁盘与内存之间交互的基本单位,其实页的本质也就是一个固定大小的内存。

InnoDB 中的页大小一般是 16KB,也就是说,一次从磁盘中读取的数据最少是 16KB,每次将内存中的数据刷新到磁盘中最少也是 16KB。

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set, 1 warning (0.00 sec)
# 16 * 1024 = 16384
#该系统变量不能在服务器运行时修改。

# InnoDB 行格式

一条记录的存放格式被称为为行格式或者记录格式,InnoDB 目前有四种行格式: COMPACTREDUNDANTDYNAMICCOMPRESSED

# 指定行格式
create table 表名 (列信息) row_format=行格式名
alter table 表名 row_format=行格式

# COMPACT 行格式

image-20220725103902849

# 变长字段长度列表

我们先创建一个表,用于之后的示例讲解

create table record_format_demo (
	t1 varchar(10),
    t2 varchar(256),
    t3 varchar(20) not null
) character set = ascii row_format = COMPACT;

变长字段指的就是 varchar (10) 这种,不能预先确定存储的数据的大小。在 InnoDB 读取数据时,如果是一个确定大小的数据,那么就能根据其大小确定偏移量从而读取出正确的数据,但是变长数据是不确定的,所以就需要保存这个变长字段(不为 NULL)的长度,来告诉 InnoDB 这个数据的大小。

所以变长字段占用的存储空间分为两部分:

  • 真正的数据内容
  • 该数据占用的字节数

在 COMPACT 行格式中,所有变长字段的真实数据占用字节数都存放在记录的开头位置 (十六进制,各字段长度之间没有空格),从而形成一个变长字段长度列表。各字段占用字节数按列的顺序逆序存放,是逆序存放!

有时用于表示长度的数据,需要占据不止一个字节。比如某一变长字段占字节 n=700 , 那么就需要用到多个字节来表示 n因为一个字节哪怕加上符号位也表示不了 n。关于使用几个字节,InnoDB 有属于自己的规则,此处引入 W,M,L 三个符号:

  • W:假设某个字符集中最多需要 W 字节表示一个字符 (也就是 show charset; 中的 MaxLen 列)

  • M:对于变长类型 VARCHAR(M) 来说,这种类型表示能存储最多 M 个字符 (是字符,不是字节),所以该类型最多能存储 M*W 个字节

  • L:该变长字段实际存储的字符数占用的字节数是 L

InnoDB 在读取记录变长字段长度列表时,会先查看表结构,如果某个字段允许存储最大字节数 M*W<=255 ,就认为只是用 1 字节表示真实数据占用的字节数。

如果 M*W > 255 :

  • L <= 127 使用 1 字节
  • L > 127 使用 2 字节

255 是 1 个字节(无符号整型)能表示的最大数,即 8 位全为 1,2^8-1=255。

127 是 1 个字节(有符号整型)能表示的最大数,即最高位 0,其他 7 位为 1,2^7-1=127

该规则主要就是用于区分长度列表中,当 M*W > 255 时,一个字节到底是一个单独的字段长度 **(L <= 127),还是半个字段长度(L > 127)**。设计者使用该字节的第一个二进制位作为标志位:如果该字节的第一个位是 0,该字节就是一个单独的字段长度,这也就是 0~127 的第一位都是 0。如果第一位为 1,那么该字节就是半个字段长度。

其实可以设定,只要 M*W > 255 就使用两个字节存储变长字段长度。之所以分的这么细,是想要节省空间,设计者的良苦用心可见一斑

如果某个字段占用字节特别多,InnoDB 可能会把该字段的值的一部分数据存放到溢出页中,变长度列表只记录留在本页中的数据长度,
所以 2 字节表示变长字段长度是完全足够的。同时,长度列表只有在有必要存在时,才会存在(存在不为 NULL 的变长字段)

对于 CHAR(M) 而言,如果使用的字符集是变长编码字符集,那么该属性也会被加入到变长列表中,并且长度为 M*W (该字符集的 MaxLen=W)。

# NULL 值列表

COMPACT 把一条记录的值为 NULL 的统一管理起来,存储到 NULL 列表中。处理过程:

  • 统计表中允许储存 NULL 的列有哪些 (比如主键和 NOT NULL 会被排除)
  • 如果表中所有列都不能存储 NULL,那么 NULL 值列表也就不存在。否则将每个允许存储 NULL 的列对应一个二进制位,位按照列的顺序逆序排列,位值为 1 时,代表该列的值为 NULL
  • MySQL 规定 NULL 列表必须用整数个字节的位表示,不足高位补 0

为什么是逆序之后会提到,先不要着急。

# 记录头信息

记录头信息由固定的 5 个字节(40bit)组成,用于描述记录的一些信息

image-20220725112044762

​ 各二进制位的详细信息

名称
预留位 1没有使用
预留位 2没有使用
deleted_flag标记该记录是否被删除
min_rec_flagB + 树的每层非叶子节点中最小目录项记录会添加该标记
n_owned页面中记录会被分为多个组,每个组最大的那个记录作为 “大哥”,“大哥 “的该标志位会记录组中记录条数
heap_no当前记录在页面堆中的相对位置,记录紧密排列的结构称为堆,前面的记录该值小,后面的值更大
record_type当前记录类型,0-- 普通记录,1--B + 树非叶子节点的目录项记录,2--Infimum,3--supremum
next_record表示下一条记录的相对位置

这么多,看不懂无所谓,现在记不住无所谓。

解释:

  • deleted_flag 中,0 表示没有删除,1 表示已经删除。尽管一条记录被标志为删除,但是并不会马上从表中移除,这是避免移除后要重新排列其他记录,造成性能损耗。所以这些被标志删除的记录,会组成一个垃圾链表,这些空间又被称为可重用空间,如我新插入的记录,垃圾链表中某一个节点(记录)放得下,就会将新纪录覆盖的那条被标志为删除的记录。
  • next_record 向我们表明记录会被排列成链表,链表是个好东西。
  • record_type 表示记录的四种类型,在目前知晓的查询算法中,二分查找已非常优秀的,所以在 MySQL 中大量使用了二分查找。那么就涉及到记录之间的大小比较,一般是通过键值比较大小。规定 record_type=2 的记录是所有记录中最小的, record_type=3 的记录是所有记录中最大的。这两个记录实际不存储任何真实数据,但是它们分别是记录链表中的起点和终点,非常重要。 record_type=1 暂时不需要知道。之后会讲解

# 记录的真实数据

我需要提醒一下,标题中的记录都是名词,不是动词。

对于表来说,记录的真实数据除了我们自己设定的数据(比如 name='cyan' 之类的),MySQL 还会为每个记录默认地添加一些列 (也称为隐藏列)。

​ MySQL 为每个记录默认添加的列

列名是否必需占用空间描述
row_id非必需6 字节行 ID,唯一标识一条记录
trx_id必需6 字节事务 ID
roll_pointer必需7 字节回滚指针,形成版本链

其实上述列名都是大写!并且这三列数据位于额外信息和真实数据中间

解释:

  • row_id 是唯一表示一条记录。如果在创建表时没有指定主键或者没有不能为 NULL 的唯一属性,反正就是,没有一个属性能够唯一表示一条记录,那么 row_id 就会被添加。
  • trx_id 与事务有关,暂时不需要了解
  • roll_pointer 是回滚指针,为了方便大家理解,这里不那么准确的讲,MySQL 支持回滚操作,也就是说我们改动一条记录后,如果想要回滚到上一个版本,就需要保存上一个版本的数据,那么 roll_pointer 就是指向上一个版本(改动前)的内存。

# 引用

本文总结于《从根上理解 MySQL》第四章 p55-p63,作者:小孩子 4919。对于有能力或者兴趣的读者,鄙人强烈推荐这本书,如果您能够通过正规渠道购买,支持作者,支持正版,支持每一颗热爱技术的心,我将感激不尽。