1、Compact行格式整体结构

MySQL数据库中的一行数据又称为一条记录,记录在存储引擎的处理下在磁盘中存储的格式称为行格式InnoDB存储引擎有4种行格式:CompactRedundantDynamicCompressed行格式,可以在创建或者修改表的sql语句中指定行格式种类。
这里重点介绍一下Compact行格式,Compact行格式的结构图如下:
InnoDB的行格式 - 图1
可见,一条完整的记录对应的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个比特位,不同的位存放不同的信息,记录头信息的结构如下:
InnoDB的行格式 - 图2
记录头信息各组成部分说明如下:

名称 大小(单位: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表示最小记录;
  • 3表示最大记录。

    2.3.5 next_record

    该字段表示从当前记录到下一条(主键值由小到大的顺序,不是插入顺序)记录的地址偏移量,比方说第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。MySQL规定Infimum记录(heap_no为0)的下一条记录就是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条记录就是Supremum记录(heap_no为1)。
    对于每个页,InnoDB会维护一条页中所有记录组成的单链表,链表的每个节点就是一条用户记录、Infimum记录或者Supremum记录,节点的尾指针就是next_record字段,链表中的记录节点的顺序是按照主键值从小到大排列的,结构图如下:
    InnoDB的行格式 - 图3
    从图中可以看出来,我们的记录按照主键从小到大的顺序形成了一个单链表。最大记录(Supremum)next_record的值为0,这也就是说最大记录是没有下一条记录了,它是这个单链表中的最后一个节点。
    如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第2条记录删掉,示意图会变成如下:
    InnoDB的行格式 - 图4
    从图中可以看出来,删除第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_idroll_pointer都是必选的。记录的真实数据实际上的结构如下:
InnoDB的行格式 - 图5
上图中,第2条记录中c3和c4列的值都为NULL,它们被存储在了前边的NULL值列表处,在记录的真实数据处就不再冗余存储,从而节省存储空间。