InnoDB行格式解析 - saviochen/mysql_docs GitHub Wiki
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格式的cluster index的中间节点记录可简单理解为cluster index key + next level page no。下面重点介绍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的最大字节数是1000,字符集单字符最大字节数是3,那么最大可能长度为3000。当变长字段的最大可能长度小于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键作为主键,如果表中连唯一键都没有定义的话,则InnoDB会为表默认添加此隐藏列作为主键。所以此列只有在无主键并且无唯一键的表中存在,此场景下InnoDB只能自己添加一个自增列DB_ROW_ID作为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 用户列
用户列与列之间没有间隔,连续存放。
2.6 二级索引记录格式
二级索引本身可能重复,可能为NULL,故其键值无法构造B+树。构造二级索引的B+树的键值为二级索引+主键索引。二级索引B+树中间节点记录可以简单理解为secondary index key + cluster index key + next level page no。二级索引记录没有DB_TRX_ID和DB_ROLL_PTR,但二级索引记录同样有记录头信息(包括delete mark,heap no,next record指针等信息),其叶子节点记录格式如下:
| 变长字段长度列表 | NULL值列表 | 记录头信息 | secondary key Fields | ... | cluster index key Fields |
|---|
二级索引叶子节点记录与中间节点记录相比少了page no指针。二级索引叶子节点记录中没有记录对应记录在cluster index中的page no,因此搜索完二级索引之后,需要通过cluster index key再搜索cluster index,才能获取完整的记录信息,此过程称为回表。
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.1 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。
5 总结
本文主要介绍了InnoDB Dynamic行格式的结构,并对比了Redundant、Compact、Compressed三种行格式的特点。