InnoDB的记录按行存储在数据页中。记录在数据页种的排布在《InnoDB页面结构》中已述及,本文重点介绍InnoDB的记录格式。
1 行格式总览
InnoDB规划了26种行格式,分别对应26种动物,首字母由A至Z:Antelope, Barracuda, Cheetah, Dragon, Elk, Fox, Gazelle, Hornet, Impala, Jaguar, Kangaroo, Leopard, Moose, Nautilus, Ocelot, Porpoise, Quail, Rabbit, Shark, Tiger, Urchin, Viper, Whale, Xenops, Yak, Zebra
。目前InnoDB支持的行格式只有Antelope, Barracuda
。而Antelope
又具体细分为Redundant
和Compact
,Barracuda也具体细分为Dynamic
和Compressed
。创建InnoDB表时,可以通过 ROW_FORMAT=XXX子句指定行格式,例如:
Create Table t (a int, b varchar(1000) not null, c char(100), d varchar(100)) CHARSET=utf8mb3 ROW_FORMAT = COMPACT;
Redundant是MySQL 5.0之前的行格式,它存储的记录是非紧凑类型的,比较占用磁盘空间。同样的页面中存储的记录行更少,索引的效率较低。目前已很少使用。Compact
、Dynamic
、Compressed
三种行格式结构比较相似。由于MySQL 5.7和8.0默认的行格式为Dynamic
,下面将展开介绍Dynamic
行格式。
2 Dynamic格式
Dynamic
行格式的级别结构如下:
变长字段长度列表 | NULL值列表 | 记录头信息 | 系统列 | Field 1 | ... | Field N |
---|
2.1 变长字段长度列表
对于Varchar、Text、Blob等这类变长的字段,其存储长度是变长的。即使对于长度相同的字段,例如CHAR(10),虽然其存储的字符是固定10个,用户输入的字符不足10个也将补齐至10个,但如果字符集是可以使用1-3个字节存储字符的utf8mb3,其存储字符的字节数也是变长的。InnoDB为了能准确划分、解析不同的字段,在每条记录的第一步部分会记录所有变长字段的长度。注意,例如Int固定长度和为空的变长字段的长度是不会记录于此的。
具体而言,每个字段的长度使用1-2字节记录。MySQL对字段由65535长度的限制也源自于此,因为2字节由16bit组成,能描述最大的数字为(2^16) - 1 = 65535。
每个字段的长度用1-2字节表示,那按什么规则区分是1个字节还是2个字节呢?在介绍规则之前先需要了解变长字段的最大可能长度的概念。变长字段的最大可能长度的计算方法为最大字符数 * 字符集最大字节数
,例如上表中列b的最大字节数是b,字符集单字符最大字节数是3,那么最大可能长度为30。当变长字段的最大可能长度
大于255时,用一个字节记录其长度。当变长字段的最大可能长度大于255时,使用1-2字节描述字段长度。具体使用1个字节还是2个字节,使用第一个字节的最高bit作为区分:如果其为0,表示只使用了一个字节,如果为1表示使用了2个字节。当只使用一个字节时,由于最高bit被用作标志,所以其能表示的真实长度的范围是[0, 127],当真实长度大于127时,需要使用2个字节表示。
单个页面大小只有16384字节,而InnoDB规定单个页面至少需要存放两条记录,那么一条记录最大不得超过8192字节。实际上,算上索引中FIl Header、Page Header、Page Directory、Fil Trailer的空间,那么在页面中存储的记录的长度更小。当记录超过限制大小时,会出现行溢出的现象,溢出页的格式将在第三节讨论。记录溢出时,对应变长字段的第一字节的第二个bit会对其进行标记,在变长字段长度列表处只存储留在本页面中的长度。至此,变长字段两个字节中的16个bit已经有两个bit用作标志(是否用两字节存储长度,是否有行外数据),还能用于描述字段长度的最大bit数为14,即最大能表示(2^14) - 1 = 16383字节,描述存储于当前数据页的记录长度仍然绰绰有余。
除上述规则之外,还需要注意的是变长字段长度列表的存储是按照字段的逆序存放的,与真实数据的存放的顺序相反。例如上例中的表t的变长字段b, c, d在变长字段列表中的顺序是d, c, b。
2.2 NULL值列表
为了节约空间,值为NULL的字段不会占用存储空间,而是通过NULL标记位记录。只有可能为NULL之的字段才有可能出现在NULL值列表中,如果一个表的所有列都用NOT NULL修饰,则该表所有记录都没有NULL值列表。
NULL值列表通过BITMAP来标识每个字段是否为空,每个可能为NULL的字段占一个bit位标识,如果字段为空,则为1,否则为0。与变长字段列表相似,所有的NULL值也按照字段顺序逆序排布。NULL列表占用的存储空间一定是8 bit的整数倍,即按字节为单位存储,如果可以为NULL的字段数不足8的倍数,在NULL值列表的高位补0。
2.3 记录头信息
记录头的信息在《InnoDB页面结构》中已有部分介绍,此处对其所有内容进行介绍。记录头包含的信息如下:
内容 | 大小 | 含义 |
---|---|---|
预留位 | 1 | 暂未使用 |
预留位 | 1 | 暂未使用 |
delete_flag | 1 | 是否删除的标识,如果删除为1,为多版本并发控制服务(Multi-Version Concurrency Control ,MVCC) |
min_rec_flag | 1 | B+树非叶子结点中每一层最小的记录会添加此标识 |
n_owned | 4 | 如果有Slot指向此记录,此字段会有值并定表此为组长记录,记录此Slot管理的记录数 |
heap_no | 13 | 记录在页面中的物理位置(堆上的位置),每申请一块记录空间,都会为其分配一个 heap_no,从前往后编号,标记删除的记录不会减小heap_no |
record_type | 3 | 记录的类型,0表示叶子结点的用户记录,1表示非叶子结点的记录,2表示Infimum记录,3表示Supremum记录 |
next_record | 16 | 下一条记录的地址,将页面内的记录串联起来 |
2.4 系统列
InnoDB聚簇索引可能会存在下述三个用户不可见的隐藏系统列:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
DB_ROW_ID | 否 | 6字节 | 行ID,唯一标识一条记录 |
DB_TRX_ID | 是 | 6字节 | 事务ID |
DB_ROLL_PTR | 是 | 7字节 | 回滚指针 |
- DB_ROW_ID:聚簇索引优先使用用户自定义的主键作为Key构建B+树,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加此隐藏列作为主键。所以此列只有在无主键并且无Unique Key的表中存在。
此列只有在无主键表中才存在。由于用户没设置主键,InnoDB只能自己添加一个自增列作为key来构建B+树。 - DB_TRX_ID:表示该行最新修改的事务ID,为MVCC判断记录可见性服务
- DB_ROLL_PTR:回滚段指针,指向记录的上一个版本,同样为MVCC判断记录可见性服务,当前记录经MVCC判断不可见时,通过该指针往前回溯记录的旧版本,找到满足可见性要求的记录返回给用户
二级索引记录没有DB_TRX_ID和DB_ROLL_PTR,所以其MVCC比较麻烦。二级索引页的Page Header有MAX_TRX_ID字段,表示更新该页面的最大事务ID。如果MAX_TRX_ID小于当前事务开启时的最小事务ID,那么万事大吉,此二级索引页面中的非标记删除的二级索引记录都是可见的。否则,就需要从二级索引访问到聚簇索引,通过聚簇索引再判断记录的可见性。
2.5 用户列
用户列与列之间没有间隔,连续存放。
3 行溢出处理
3.1 行溢出时记录的格式
当变长的字段数据过长,导致索引页无法容纳两条记录,InnoDB会将过长的字段内容存储到外部存储页(blob page)。不同行格式在此处的处理略有不同。Antelope
(Redundant
和Compact
)会Field内容处存储数据内容的768字节 + 行外数据等地址指针。而Barracuda
(Dynamic
和Compressed
)只在Field内容处记录行外数据等地址指针。
行外数据等地址指针占20字节,格式如下:
名称 | 大小 | 内容 |
---|---|---|
BTR_EXTERN_SPACE_ID | 4 | 外部存储页的space id |
BTR_EXTERN_PAGE_NO | 4 | 外部存储页的页码 |
BTR_EXTERN_OFFSET | 4 | 外部存储页的页内偏移。 |
BTR_EXTERN_LEN | 8 | 数据的总大小 |
- BTR_EXTERN_OFFSET的取值分两种情况:当外部存储页不是压缩页时,该值为38。其指向外部存储页的Blob Header;当外部存储页时压缩页时,该值为12,指向Fil Header部分的FIL_PAGE_NEXT。
- BTR_EXTERN_LEN尽管有8个字节可以存储BLOB数据的总大小,但实际上只使用了最后4个字节。这意味着在InnoDB中,单个BLOB字段的最大大小目前为4GB。
3.2 非压缩外部存储页结构
在非压缩页格式中,外部存储页的管理结构由FIl Header、Blob header、Blob data、Fil Trailer组成,溢出行中地址将指向Blob header。(关于Fil Header的介绍详见《InnoDB页面结构》)。非压缩外部存储页的结构如下:
Blob header的组成如下:
内容 | 大小 | 含义 |
---|---|---|
BTR_BLOB_HDR_PART_LEN | 4 | 当前页中存储的字段的长度 |
BTR_BLOB_HDR_NEXT_PAGE_NO | 4 | 如果当前页面未能存储所有字段的全部数据,会指向下一个外部存储页面的Page no。 |
3.3 压缩外部存储页结构
如果外部存储页为压缩格式,其直接由Fil Header、压缩数据、Fil Trailer组成。溢出行中地址将指向Fil Header中的FIL_PAGE_NEXT(页内偏移为12)。压缩外部存储页的结构如下图所示:
4 其他行格式对比
4.2 Redundant
如前所述,Redundant
是非紧凑型行格式,比较占用磁盘空间。Redundant
行格式与Dynamic
格式的不同之处在于并没有区分定长和变长字段,而是将所有列占用的存储空间都逆序存储在字段长度偏移列表中。并且 Redundant
格式并不存在NULL值列表,使用字段长度值的第1位来判断字段是否为空,如果第1位为1,则为空。因为第1位用来记录字段是否为NULL,所以一个字节所能表示的最大长度为127。
Redundant
格式的记录头占用了6个字节,分为了9部分,相较于Dynamic
格式多了n_field和1byte_offs_flag字段,少了record_type字段,格式如下所示:
名称 | 大小 | 内容 |
---|---|---|
预留位 | 1 | 暂未使用 |
预留位 | 1 | 暂未使用 |
delete_flag | 1 | 是否删除的标识,如果删除为1 |
min_rec_flag | 1 | B+树非叶子结点中每一层最小的记录会添加此标识 |
n_owned | 4 | 如果有slot指向此记录,此字段会有值,记录此slot管理的记录数 |
heap_no | 13 | 记录在页面中的物理位置(堆上的位置),每申请一块记录空间,都会为其分配一个 heap_no,从前往后编号 |
n_field | 10 | 记录中列的数量 |
1byte_offs_flag | 1 | 标识字段长度偏移列表中字段的长度用1个字节还是2个字节来表示,如果所有字段长度小于127,则用一个字节表示,如果大于127,则用两个字段表示 |
next_record | 16 | 下一条记录的地址,将页面内的记录串联起来 |
4.2 Compact
Compact是一种紧凑类型的存储格式,与Dynamic
类型的存储格式基本一致。如第三节所述,作为Antelope
,其溢出行的处理方式是在索引页存储变长字段的前768字节的数据+外部存储页指针,因此其变长字段长度为768+20。与Redundant
格式相比,Compact
行格式减少了约20%的行存储空间。
4.3 Compressed
Compressed
类型与Dynamic
类型拥有相同的存储特性和功能,不同之处在于使用压缩算法对页面进行压缩,包括溢出页。优点在于可以节约存储空间,但是在查找数据时需要先解压才行,会消耗更多的CPU资源。
Compressed
行格式必须在建表时指定,而且需要同时指定KEY_BLOCK_SIZE。KEY_BLOCK_SIZE会控制压缩后页面的大小,指定的大小必须小于当前默认数据页的大小。如果没有指定KEY_BLOCK_SIZE,则会自动设置为默认数据页大小的一半。如果要使通用表空间包含压缩表,必须指定FILE_BLOCK_SIZE选项,如果小于当前默认数据页的大小,会自动设置为Compressed
格式。其中FILE_BLOCK_SIZE的单位为Byte,KEY_BLOCK_SIZE的单位为KB。