MySQL之行格式、页结构
前言
关于为何要了解MySQL
的物理实现:
其实像B+索引,多版本并发控制(MVCC)等MySQL常问的技术知识点都是会对应到具体的物理实现上,如果不了解MySQL到底怎么存储数据,不清楚每个数据行中有什么结构,不清楚B+树中的一个节点对应什么物理结构,又怎么算了解了MySQL
从图开始理解
以下面这段创建代码为例:
mysql> CREATE TABLE format_demo (
-> c1 VARCHAR(10),
-> c2 VARCHAR NOT NULL,
-> c3 CHAR(10),
-> c4 VARCHAR(10)
)
那么现在这个表在我们眼中是这样的:(插入了两条数据)
mysql> SELECT * FROM record_format_demo;
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
2 rows in set (0.00 sec)
mysql>
对应在磁盘中表又是怎么样存储的呢?
行格式
所谓表的构成,实际就是一行行的数据,所以在磁盘中表是按行数据进行存储的。那么在磁盘中是一整个表的数据都连续放在一起么?显然不可能,思考数据分页的方法,MySQL
也是按照分页的方式将一个表的数据拆分开存放。以InnoDB
来说:
- 将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
关于页的概念先放到这之后再说。
所以现在我们前进了一大步,起码知道磁盘里的表大概长这样:
再回到行格式上,MySQL
中涉及的有这4种:
COMPACT行格式
Redundant行格式
Dynamic行格式
Compressed行格式
主要介绍最重要的COMPACT格式,首先它长这样:
可以看到MySQL
除了记录用户提供的信息之外还记录了相当的额外信息,这些信息可以分为3
类:
变长字段长度列表
NULL值列表
记录头信息
简要说明这些额外信息:
变长字段长度列表将真实数据中的每个非空列按其数据字节长度的逆序存放起来
空间分配:长度不是固定的,取决于有多少数据
NULL值列表中储存了所有没有设置NOT NULL
的字段(列),并用一个2进制位表示该列的NULL
状态,同时这个列表也是按列顺序的倒序排列的,也就是如果该行数据中对应第1
,3
,4
个字段可以为空(没有设置NOT NULL
)那么NULL值列表就长这样:
需要说明的是,MySQL
中无论是定长数据还是非定长数据都可以设置对NULL
的控制,所以如果该列不为空那么该列数据的相关信息就存在变长字段长度列表中。
空间分配:同时NULL值列表是按整数倍字节分配空间的,不足的位置补上0
.
记录头信息存储的都是和行数据控制相关的内容:
空间分配:固定5
个字节40
个二进制位
每段内容记录信息如下:(不做详细解释了,了解大概即可)
名称 | 大小(Bit) | 描述 |
---|---|---|
预留位 | 1 | - |
预留位 | 1 | - |
delete_mash |
1 | 删除标记位 |
min_rec_mask |
1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 | 表示当前记录拥有的记录数 |
heap_no |
13 | 表示当前记录在记录堆的位置信息 |
record_type |
3 | 表示当前记录的类型,0 表示普通记录,1 表示B+树非叶子节点记录,2 表示最小记录,3 表示最大记录 |
next_record |
16 | 表示下一条记录的相对位置 |
记录的真实数据
说完记录中的额外信息,那么记录里的真实数据真的只有用户定义的数据么?
显然不是,MySQL
会自动为每个行数据增加一些额外的列,例如DB_ROW_ID
,DB_TRX_ID
, DB_ROW_PTR
分别表示数据行的行id
, 行的事务id
, 指向下一个版本数据行的指针
,其中事务id和指向下一版本的指针实际都是关系到MySQL
中多版本并发控制的具体实现。
所以到目前为止,我们已经完整了解了一条数据行到底长什么样:
关于定长和非定长类型
VARCHAR()和CHAR()类型的大致区别,在根据建表时设定的不同字符集下(字符在字符集中对应占字节L
),传入类型中的Maxlen
会限定给CHAR(Maxlen)
至少分配ML的空间,而VARCHAR(Maxlen)
则是完全根据数据具体字符个数*来计算分配。计算分配的规则不在这里说明。
页结构
下面是页结构的示意图:
可以在中间找到我们刚刚讨论的行数据位于User Records
中。
上述内容简述功能如下:
名称 | 中文名 | 占用空间 | 描述 |
---|---|---|---|
File Header |
文件头部 |
38 字节 |
页的一些通用信息 |
Page Header |
页面头部 |
56 字节 |
数据页专有的一些信息 |
Infimum + Supremum |
最小记录和最大记录 |
26 字节 |
两个虚拟的行记录 |
User Records |
用户记录 | 不确定 | 实际存储的行记录内容 |
Free Space |
空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory |
页面目录 | 不确定 | 页中的某些记录的相对位置 |
File Trailer |
文件尾部 |
8 字节 |
校验页是否完整 |
其中用户记录开始是没有的,当插入行数据时会使用空闲空间,当空闲空间装满之后就要申请新的页了
继续说明行记录在页中怎么组织又需要使用到之前提到的行中的头信息
大致如下:
-
heap_no
属性表示当前记录在页中的位置,用户插入的数据一般从2
开始排序,为什么是2
呢?因为
0
,1
是页中固有的最大和最小行记录,分别指向页最大和最小数据。这里其实涉及到了MySQL
B+树的物理实现过程,简单来说就是页其实就是B+树的一个物理节点,而B+树中所有节点是顺序排列的,这个排列的顺序就是按照表的唯一主键来进行的(一般会设置一个与业务无关的逻辑主键,并且自增)。所以这两个多余的记录就可以理解成节点链表中的头尾节点,用于快速遍历节点。 next_record
记录了从当前记录到下一行数据的地址偏移量
那么现在页中的行数据就是这样串起来的:
这里再提供一个简图说明一个页是B+树中的一个节点这句话的意义:
页目录
所以B+树中的索引是怎么在页中实现的呢?这里就涉及到页中另外一个关键的内容:页目录
页目录就是MySQL在页中可以快速检索数据的保障。如果没有页目录,那么定位到了一个页不就只能顺序遍历一次链表了。当然,页目录得以实现的基础也是行数据是按主键进行排序存放的。
而目录的制作过程大致是这样的:
将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的
n_owned
属性表示该记录拥有多少条记录,也就是该组内共有几条记录。将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近
页
的尾部的地方,这个地方就是所谓的Page Directory
,也就是页目录
(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽
(英文名:Slot
),所以这个页面目录就是由槽
组成的。
那么最小只有一个分组的概况如下:
可以观察到最小记录的n_owned
值为1,而最大记录的n_owned
值为5。这关系到每个分组是如何设计的:
对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。所以分组是按照下边的步骤进行的:
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
之后每插入一条记录,都会从
页目录
中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned
值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在
页目录
中新增一个槽
来记录这个新增分组中最大的那条记录的偏移量。
所以最终一个有多个分组的页面结构就长这样:
File Header和File Trailer
不出意料Header中保存了这个页中的一些信息,而File Trailer中保存了指向下一个页的指针,用于串联页面,也就是B+树。
全文内容总结自:掘金小册《MySQL是怎样运行的》