MySQL存储引擎InnoDB行格式
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。
可以在创建或修改表的语句中指定行格式:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
Compact
一条完整的记录可以被分为记录额外信息
和记录真实数据
两大部分。
记录额外信息
记录额外信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是变长字段长度列表
、NULL值列表
和记录头信息
。
变长字段长度列表
在Compact行格式中,把所有变长字段的真实数据占用字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用字节数(十六进制表示)按照列的顺序逆序存放。
用1个还是2个字节来表示真实数据占用字节数,InnoDB有它的一套规则:
- 假设某个字符集中表示一个字符最多需要使用的字节数为
W
,也就是使用SHOW CHARSET
语句结果中的Maxlen
列,比方说utf8
字符集中W
就是3
,gbk
字符集中W
就是2
,ascii
字符集中W
就是1
- 对于变长类型
VARCHAR(M)
来说,这种类型表示能存储最多M
个字符(注意是字符不是字节),所以这个类型能表示的字符串最多占用的字节数就是M × W
- 假设实际存储的字符串占用字节数是
L
所以确定使用1个字节还是2个字节表示真正字符串占用字节数规则就是这样:
- 如果
M×W <= 255
,那么使用1个字节来表示真正字符串占用字节数 如果
M×W > 255
,则分为两种情况:- 如果
L <= 127
,则用1个字节来表示真正字符串占用字节数 - 如果
L > 127
,则用2个字节来表示真正字符串占用字节数
- 如果
InnoDB在读记录的变长字段长度列表时会先查看表结构,如果某个变长字段允许存储的最大字节数大于255时,该怎么区分它正在读的某个字节是一个单独的字段长度还是半个字段长度呢?InnoDB使用该字节的第一个二进制位作为标志位:如果该字节的第一个位为0,那该字节就是一个单独的字段长度(使用一个字节表示不大于127的二进制的第一个位都为0),如果该字节的第一个位为1,那该字节就是半个字段长度。
需要注意的是,变长字段长度列表中只存储值为非NULL的列内容占用长度,值为NULL的列长度是不储存的。
对于CHAR(M)类型的列来说,当列采用的是定长字符集时,该列占用字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用字节数也会被加到变长字段长度列表。
最后,并不是所有记录都有这个变长字段长度列表部分,比方说表中所有列都不是变长数据类型,这一部分就不需要有。
NULL值列表
如果把NULL值都放到记录真实数据
中存储会很占地方,所以Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表中,它的处理过程是这样的:
- 首先统计表中允许存储NULL的列有哪些
- 如果表中没有允许存储NULL值的列,则NULL值列表也不存在,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义:值为
1
时,代表该列的值为NULL
;值为0
时,代表该列的值不为NULL
- MySQL规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补
0
。如果一个表中有9列允许为NULL,那这个NULL值列表部分就需要2个字节来表示
记录头信息
由固定的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 | 表示下一条记录的相对位置 |
delete_mask
这个属性标记当前记录是否被删除,值为0代表记录并没有被删除,为1代表记录被删除掉了。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表
,在这个链表中的记录占用的空间称之为所谓的可重用空间
,之后如果有新记录插入到表中的话,可能会把这些被删除记录占用的存储空间覆盖掉。
heap_no
这个属性表示当前记录在记录堆的位置信息,InnoDB会自动给每个页里边儿加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录(heap_no值为0),一个代表最大记录(heap_no值为1)。
next_record
非常重要,它表示从当前记录真实数据到下一条记录真实数据的地址偏移量。比方说第一条记录的next_record值为32,意味着从第一条记录真实数据的地址处向后找32个字节便是下一条记录真实数据。
记录真实数据
记录真实数据除了自己定义的列数据外,MySQL会为每个记录默认添加一些列(也称为隐藏列),具体如下:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6字节 | 行ID,唯一标识一条记录 |
transaction_id | 是 | 6字节 | 事务ID |
roll_pointer | 是 | 7字节 | 回滚指针 |
实际上这几个列的真正名称是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。
需要注意的是,变⻓字符集的CHAR(M)类型列要求⾄少占⽤M个字节,⽽VARCHAR(M)却没有这个要求。
Redundant
该行格式是MySQL 5.0之前使用的一种行格式,也就是说它已经非常老了,这里就不介绍了。
行溢出
对于VARCHAR(M)类型的列最多可以占用65535个字节,其实这个65535除了列本身的数据之外,还包括一些其他数据(storage overhead)。如果该VARCHAR类型列没有NOT NULL属性,那最多只能存储65532
个字节数据,因为真实数据长度可能占用2个字节,NULL值标识需要占用1个字节。
在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录真实数据处只会存储该列的一部分数据,剩余数据分散存储在几个其他页中,然后记录真实数据处用20个字节存储指向这些页的地址,从而可以找到剩余数据所在的页。这个过程也叫做行溢出,存储剩余数据所在页也被称为溢出页。
发生行溢出的临界点是什么呢?也就是说在列存储多少字节数据时会发生行溢出
?其实,不用关注这个临界点是什么,只要知道如果一个行中存储了很大的数据时,可能发生行溢出的现象就行了。
Dynamic和Compressed行格式
MySQL 5.7默认行格式就是Dynamic,这俩行格式和Compact行格式挺像,只不过在处理行溢出数据时有点儿分歧,它们不会在记录真实数据处存储字段真实数据的前768
个字节,而是把所有字节都存储到其他页面中,只在记录真实数据处存储其他页面的地址。
Compressed和Dynamic不同的一点是,Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。