127的二进制是多少(InnoDB行结构详解)
InnoDB默认的行格式是Dynamic,本文则以Compact为例介绍,两者本身也是及其相似的。我们先来回顾一下Compact行结构,如下:

Compact格式行
变长字段长度列表
“变长”顾名思义这里只存储不固定长度的字段长度值,MySQL变长的字段类型有,VARCHAR、VARBINARY、TEXT、BLOB等类型。
接下来为了方便阅读我们将变长字段长度列表简称为变长列表。
目的
Compact行中记录头信息、RowID、事务ID、回滚指针都是有固定字节数的,但是列数据占用的实际长度是无法计算的,因为数据列中可能存在变长的字段类型,那么我们就无法计算整个行的精确长度了,那么就不知道行与行之间的边界了。因此我们需要通过变长列表来记录变长的字段所占用的字节数。
规则
- 变长列表的记录是按照变长列逆序存放的,在上一篇文章中有示例。
- 如果变长字段的值为NULL,则会记录在NULL值列表,变长列表不记录。
- 变长字段的定义长度<=255字节使用1字节存储长度,否则用2字节。
例如:创建列类型为varchar(m),字符集为utf8,则一个字符占用3字节,则定义长度为m*3<=255,其它字符集不在赘述(MySQL5.0之后将varchar(m)的m由字节数调整为字符数)。
温馨提示:
从这个规则可以看出,在设计列时,非必要可以减少变长字段的使用,这样可以有效减少变长列表长度。
如果使用到变长类型如varchar(m),其中m的值尽量设置为预计存储字符数的最大长度,不要定义太过随意,可能会影响行的相关计算性能,因为小于等于255字节,说明其长度不会超过1111 1111(一字节表示的最大无符号整数),则直接使用1字节,当大于255时会多一条规则计算,即规则4。
4. 变长字段真实长度<=127字节使用1字节存储长度,否则为两字节。
为什么是127呢?
InnoDB设计者非常的睿智,他将第一个字节的最高位一位用来标记是否是连续的两字节表示变长字段长度,即高位为0表示字段长度占用1字节,若为1,则需要再向前读取一个字节。但是有个特殊情况,规则3中提到的255字节,255二进制为1111 1111,这就和规则4冲突了,因此需要优先判断规则3。
127的二进制为0111 1111,那么再加1则变成128(1000 0000),按照规则,必须存两个字节,即00 80。最高位既要作为标识位,又要作为数据位,那么127就是边界值了,否则我们需要对最高位做翻转才能得到实际的长度值,但是翻转又会导致改变其作为数据位时的值(比如长度01 ff和01 7f都要记录为01 ff,那么这两个值就分不清了)。
下面我们着重讲一下变长列表的读取方法。
如何读取变长列表?
变长列表的存储长度值是连续的,我们如何区分出每个长度值呢?
我们先列出一个结论,带着这个结论看下面的示例:
结论:变长列表为逆序存放逆序读取,逆序读取的第一个字节,首先需要判断字段定义长度是否超过255字节,若没有超过则读取的第一字节即为长度值,否则,如果为1则表示还要向前读取一个字节,两个字节值则为变长列长度;若高位为0则该字节值即为变长字段的长度。
这是我建立的一张表(row_format=COMPACT engine=innodb charset=ascii),有5个列,插入的数据如下图:

表结构

表数据
然后通过命令行的一些列操作(具体怎么操作的,我后续出个教程,今天肝不动了),得到了其ibd文件的数据如下:

ibd文件(16进制)
然后我们可视化一下,得到如下格式(为了减少文字表述,我直接在图上做了文字标注):

Compact格式数据行实际存储
我们重点看一下变长列表:02 03代表什么呢?我们看一下变长字段有col1、col3、col4,根据规则2我们把col3过滤掉,剩下col1、col4,值分别为aaa、cc,按照规则1可得出02 03。假如我们在表最前面插入一个新列col0:varchar(512),其值等于128字节,则变长列表变为:02 03 00 80。
我们找到一个数据行要么是通过槽,要么是通过上一条行记录头中的next_record指针,这两者都是指向的行记录第一个列的位置。如果知道了第一个列的位置,我们就可以向左侧搜索,找到的就是记录头(固定大小)和NULL标记位(可计算出占用大小,下面会讲到),减去上他们占用的字节数就可以计算出变长列表的最右侧起始位置了。上面提到变长列表为:02 03 00 80,我们首先读到80,首先通过表的定义信息获取第一个变长列的定义长度为512,则直接判断高位,发现其高位为1,则读下一个字节,最终其实际长度00 80。

行读取规则
有没有发现(结合上图看),其检索方式就是双指针,一个向左读变长列表,一个向右读各个列,这里左指针读取变长列表中的列长度值顺序是和右指针读取列值的顺序是一样的,这就是为什么变长列表要逆序存储的原因。综上所述,这也是为什么变长列表要逆序读取的原因。
上面讲得有那么点啰嗦了,但是为了讲清楚,你们忍一下,务必认真读一遍。
NULL标记位
前面讲了变长列表,其中是没有记录NULL的变长列的,那么NULL就会做一个非常重要的补充记录,记录所有允许为NULL且列值实际为NULL的列。
目的
为了让我们能够正确的读取列值,我们通过n个字节来记录所有NULL值的列。
规则
- 只有字段值允许为NULL的字段才会记录在NULL值标记位中。
- NULL值标记位是有0~n字节组成的,每个字节中的位就代表了列是否为NULL值,即0代表不为NULL,1代表为NULL。
- 当所有列都不允许为空时,则没有该标记位。
温馨提示:建表的时候尽量减少允许NULL字段的出现,最好是都不允许为NULL,这样每个记录行会节约最少1个字节的存储空间。
4. NULL值标记位中位是按照列的逆序存储的,读取方向与变长列表一样,见下图:

NULL值标记位读取规则
5. NULL标记位的计算逻辑为:向上取整(允许为NULL的列数量/8)=NULL标记字节数。
固定头信息
头记录信息中包含了该行在页中的位置、行相对于上一条行记录的位置等一系列位置及状态信息。
名称 | 占用位数(bit) | 功能描述 |
预留位 | 1 | 预留位 |
预留位 | 1 | 预留位 |
delete_flag | 1 | 删除标识 |
min_rec_flag | 1 | 非叶子节点最小目录项记录标识 |
n_owned | 4 | 若为分组的头号记录,则记录组内的记录数,每组4-8个记录 |
heap_no | 13 | 当前记录在页中的相对位置 |
record_type | 3 | 记录类型,0:表示普通记录;1:表示B+树非叶子节点的目录项记录;2:表示Infimum记录;3:表示Supremum记录; |
next_record | 16 | 下一条记录的相对位置 |
隐藏列
隐藏列例如:row_id(行的Primary Key,若没指定则有会生成该列)、trx_id(事务ID)、roll_pointer(回滚指针),这些都是MySQL事务及MVCC实现必不可少的数据,由MySQL内部维护,后续在介绍事务及过版本控制协议MVCC会详细讲到。
名称 | 占用字节数 | 功能描述 |
row_id | 6 | 行号 |
trx_id | 6 | 事务ID |
roll_pointer | 7 | 回滚指针 |
Dynamic格式与Compact格式
这里简单介绍一下Dynamic格式与Compact格式异同:
- Dynamic格式支持对大型动态字段的页外存储能力,而聚集索引节点只需要记录其溢出页面的指针,长度20字节(这也是上面没有提到的Compact格式可能存在跨页存储的问题,解决方案两种格式都是类似的)。
- 因其上述特性,其对大型索引键前缀的支持能力就显而易见了(索引值页外存储,最大支持3072字节,这个长度受innodb_large_prefix配置控制)。
- Dynamic格式对小型字段的支持仍然与Compact完全一致。
Infimun和Supremum行
借这篇文章在这里顺便介绍一下,这两个记录就是确定一个页内行记录的存储边界的,在图:ibd文件(16进制)中也可以直观看到,他们在所有数据行的最前面,他们的next_recoerd指针分别指向内内第一个行记录与最后一个行记录。
总结
- 创建表的时候,尽量减少动态列的使用(尤其是TEXT类型),可以使用char或者其它类型代替,既可以节约存储空间,又可以提升行检索计算性能。
- 如果大量使用到变长字段类型,也需要合理地计算其字符数,不要定义太过随意,以免影响计算性能。
- 尽量不要建允许为NULL的列,这样既可以减少行存储空间,又可以提升计算性能。