# 前言
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 目前有四种行格式: COMPACT
, REDUNDANT
, DYNAMIC
, COMPRESSED
。
# 指定行格式
create table 表名 (列信息) row_format=行格式名
alter table 表名 row_format=行格式
# COMPACT 行格式
# 变长字段长度列表
我们先创建一个表,用于之后的示例讲解
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)组成,用于描述记录的一些信息
各二进制位的详细信息
名称 | |
---|---|
预留位 1 | 没有使用 |
预留位 2 | 没有使用 |
deleted_flag | 标记该记录是否被删除 |
min_rec_flag | B + 树的每层非叶子节点中最小目录项记录会添加该标记 |
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。对于有能力或者兴趣的读者,鄙人强烈推荐这本书,如果您能够通过正规渠道购买,支持作者,支持正版,支持每一颗热爱技术的心,我将感激不尽。