1、Compact行格式整体结构
MySQL
数据库中的一行数据又称为一条记录,记录在存储引擎的处理下在磁盘中存储的格式称为行格式。InnoDB
存储引擎有4种行格式:Compact
、Redundant
、Dynamic
和Compressed
行格式,可以在创建或者修改表的sql
语句中指定行格式种类。
这里重点介绍一下Compact
行格式,Compact
行格式的结构图如下:
可见,一条完整的记录对应的Compact行格式内容分为两大部分:记录的额外信息和记录的真实数据,下面分别介绍一下这两部分。
2、记录的额外信息
记录的额外信息包含三部分:变长字段长度列表、NULL值列表和记录头信息,下面简单介绍一下前面两个,重点介绍一下记录头信息。
2.1 变长字段长度列表
MySQL
支持一些不定长的数据类型,比如VARCHAR(M)
,把拥有这种数据类型的列称为变长字段。变长字段占用的存储空间分为两部分:真正的数据内容和占用的字节数。在Compact
行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放。
2.2 NULL值列表
表中的某些列可能存有NULL
值,如果把这些NULL
值的列也存储到“记录的真实数据”中会很浪费内存空间,Compact
行格式把这些值为NULL
的列统一管理起来,存储到NULL
值列表中。注意:主键列、被NOT NULL修饰的列都是不可以存储NULL值的,所以在统计的时候不会把这些列算进去。
2.3 记录头信息
记录头信息用来描述该记录的元数据信息,类似于头域“headers
”,记录头信息包含5个字节,即40个比特位,不同的位存放不同的信息,记录头信息的结构如下:
记录头信息各组成部分说明如下:
名称 | 大小(单位:bit) | 描述 |
---|---|---|
预留位1 |
1 |
没有使用 |
预留位2 |
1 |
没有使用 |
delete_mask |
1 |
标记该记录是否被删除 |
min_rec_mask |
1 |
B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 |
表示当前记录所在的分组拥有的记录数 |
heap_no |
13 |
表示当前记录在记录堆的位置信息 |
record_type |
3 |
表示当前记录的类型,0 表示普通记录, 1 表示B+树非叶子节点记录, 2 表示最小记录, 3 表示最大记录 |
next_record |
16 |
表示下一条记录的相对位置 |
2.3.1 delete_mask
标删,跟我们对象元数据的dlc是一个含义。这个字段表示当前记录是否被删除,占用一个二进制位,值为0时表示当前记录没有被删除,值为1时表示记录被删除了。标删的意义是如果真的把记录从页中的User Records分区中删掉,其他记录在磁盘上重新排列需要消耗性能,所以打上标删标识该记录已经被删除。
所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
2.3.2 n_owned
在页目录里这个字段会涉及,表示生成页目录时会划分几个组,组中的记录按照主键值从小到大排列,每个组最后一条记录的记录头信息里n_owned字段表示该分组内有多少条记录。之所以记录这个n_owned信息,是在快速查找记录中会用到,具体见下一节页的介绍。
2.3.3 heap_no
这个字段表示当前记录在页中序号位置,从2开始往后表示具体的记录。heap_no为0和1的记录叫虚拟记录,heap_no为0对应的是最小记录(infimum),heap_no为1对应的是最大记录(supremum),这两条记录不是用户自定义的记录,因此不放在页的User Records部分,而是单独放在Infimum + Supremum部分。这里说明一下记录的比较大小的定义:对于一条记录,比较记录的大小就是比较记录的主键的大小。
2.3.4 record_type
该属性表示当前记录的类型,一共有4种类型,如下:
- 0表示普通记录;
- 1表示B+树非叶子节点记录,即目录项记录;
- 2表示最小记录;
-
2.3.5 next_record
该字段表示从当前记录到下一条(主键值由小到大的顺序,不是插入顺序)记录的地址偏移量,比方说第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。
MySQL
规定Infimum
记录(heap_no为0)的下一条记录就是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条记录就是Supremum
记录(heap_no为1)。
对于每个页,InnoDB会维护一条页中所有记录组成的单链表,链表的每个节点就是一条用户记录、Infimum
记录或者Supremum
记录,节点的尾指针就是next_record字段,链表中的记录节点的顺序是按照主键值从小到大排列的,结构图如下:
从图中可以看出来,我们的记录按照主键从小到大的顺序形成了一个单链表。最大记录(Supremum)
的next_record
的值为0
,这也就是说最大记录是没有下一条记录
了,它是这个单链表中的最后一个节点。
如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第2条记录删掉,示意图会变成如下:
从图中可以看出来,删除第2条记录前后主要发生了如下变化: 第2条记录并没有从存储空间中移除,而是把该条记录的
delete_mask
值设置为1
;- 第2条记录的
next_record
值变为了0,意味着该记录没有下一条记录了; - 第1条记录的
next_record
指向了第3条记录; - 最大记录的
n_owned
值从5
变成了4
,因为该分组中少了第2条记录,最大记录的记录头信息里的n_owned值会减一。
主键值为2的记录被我们删掉了,但是存储空间却没有回收,如果我们再次把这条记录插入到表中,InnoDB
并不会因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。
3、记录的真实数据
除了我们真正要存储的数据对应的列之外,InnoDB
引擎会为每条记录添加额外的列,这些额外的列又叫做隐藏列,隐藏列有三种,如下:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id |
否 | 6 字节 |
行ID,唯一标识一条记录 |
transaction_id |
是 | 6 字节 |
事务ID |
roll_pointer |
是 | 7 字节 |
回滚指针 |
这里需要提一下InnoDB
表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique
键作为主键,如果表中连Unique
键都没有定义的话,则InnoDB
会为表默认添加一个名为row_id
的隐藏列作为主键。
注意上面三种隐藏列,只有row_id
是可选的,transcation_id
和roll_pointe
r都是必选的。记录的真实数据实际上的结构如下:
上图中,第2条记录中c3和c4列的值都为NULL
,它们被存储在了前边的NULL值列表处,在记录的真实数据处就不再冗余存储,从而节省存储空间。