在介绍索引的文章已经知道。InnoDB的表数据被拆分成不同的数据页上,默认一个数据页大小是16kb,分布在聚簇索引的叶子节点上。被挂在B+树上。一条行记录除了要保存每列具体数据值还会有一些标识位信息。另外对于超长数据存储也有特殊处理。
那么具体到一行数据多个列数据是怎么组织的呢?InnoDB存储引擎提供4种行格式: REDUNDANT, COMPACT, DYNAMIC和COMPRESSED。
下图是4种行格式的对比:
Row Format | 紧凑存储 | 加强可变长度存储 | 长索引key支持 | 是否支持压缩 | 支持表空间类型 |
---|---|---|---|---|---|
REDUNDANT | No | No | No | No | system, file-per-table, general |
COMPACT | Yes | No | No | No | system, file-per-table, general |
DYNAMIC | Yes | Yes | Yes | No | system, file-per-table, general |
COMPRESSED | Yes | Yes | Yes | Yes | file-per-table, general |
这4种行格式其实后面的三种在数据格式上是一样的,只有REDUNDANT与其它三种格式不同。REDUNDANT是5.0之前的数据格式,已经过时了。从5.7之后版本默认行记录格式是DYNAMIC。
行数据的基本格式
行数据基本格式大致如下:
其中蓝色部分是行真正存储的数据区,是所有的列数据byte按顺序紧密连接在一起。黄色区域是非数据区,都是一些标识位信息。这一块几种行格式会有两种表现形式。
ROW_ID
如果没有主键,会有一个6字节长度的ROW_ID用来标识该列。
TRX_ID和ROLL_POINTER
每一行还有一个6字节的事务ID和7字节长度的回滚指针,这都是用来事务操作的。
头信息
头信息REDUNDANT 占用6个字节,其它占用5个字节。头信息中会有一些标识为,比如会存储下一行记录的地址信息。行是否被删除等等。
NULL列标识位
null列标识位REDUNDANT 格式是没有的,只有其它三种有。这个就是使用一个bit向量来标识每一列(这里的每一列是指的在表定义时候该列没有非空约束)是否为空,如果为NULL,则对应bit位为1。这部分字节长度由表中可空列的数量来决定,如小于8列则用一个字节长度就可以标识,如9~16长度则需要两个字节,依次类推这个也很好理解。
对于NULL的列,REDUNDANT 格式下如果列是可变长度不会在数据区进行存储,如果是定长列则会在蓝色数据区也占用相同的定长空间。而其它三种数据格式NULL值在数据区不占空间。
变长字段长度标识列表
这一部分是用来干什么的呢?想想前面的蓝色数据区是紧凑的字节连接在一起的,对于变长列数据长度又不是固定的,在数据查询抽取的时候怎么拆分每列数据呢?这里这部分就是用来标识每个变长列的字节长度的。只不过REDUNDANT和其它三种这里存储变长长度值格式又些不同。
REDUNDANT采用长度偏移量来表示。如果所有列长度小于127则每列长度使用一个字节来表示,否则使用两个字节。这里说的字节长度是每一列的长度偏移量都使用这么长的字节数。举个例子有三个列数据长度为10、20、30字节。长度总和为60则,每列长度偏移量使用1个字节就可以了。具体存储偏移量(0,10,30)。在header头部分有标识位标识使用1个字节还是2个字节。
其它三种,对于所有的非空可变长度列。如果列定义最大字节数不超过255字节(注意这里是列定义,也就是varchar(n)这样),则使用1个字节表示就可以了,因为一个字节整数最大值255。如果列定义允许最大长度超过255,这就需要分两种情况了,1、如果实际数据长度小于127字节,则用1个字节来表示其长度,2、若大于127字节则采用2字节来表示。这都是为了能省一个字节就省一个字节,要不然定义超过255直接两个字节长度就可以了。也没有必要过多去追究太细。
超长数据处理
InnoDB默认一个数据页大小是16kb,一个页上至少要存储2条记录。如果一行数据小于一个数据页的一半,则整行数据会存储在页内,如果超过了页的一半,可变长度列会被依次摘出存储在额外的空间(off-page)直到剩下行数据大小小于页的一般。这些单独分配的磁盘页称为溢出页(overflow pages)。这样的列称为页外列(off-page columns)。off-page columns的值存储在overflow pages的单链表中,每个这样的列都有自己的一个或多个overflow pages列表。对这4种行格式对于超长数据处理有一些不同
REDUNDANT和COMPACT 模式对于超长可变数据列,如果实际数据大小大于768字节,则在行数据上只会保留前768字节和对应溢出页的指针,超过的部分存储在溢出页。另外对于定长超过768同样按照这种方式处理。
DYNAMIC和COMPRESSED 模式对于实际数据大小超过768字节,则整个超长列数据都会存储在溢出页上,行记录上只保存一个到溢出页的指针。
长索引KEY
DYNAMIC和COMPRESSED两种模式还支持长索引key。也就是在一些超长字段上设置索引。索引key前缀最大长度只3072 字节。
配置行格式
默认行记录格式使用innodb_default_row_format变量来控制,如果在创建一个表的时候没有指定row_format,则默认使用ROW_FORMAT=DEFAULT来指定行记录格式。
查看当前默认行记录格式
SELECT @@innodb_default_row_format;
设置默认行记录格式
mysql> SET GLOBAL innodb_default_row_format=DYNAMIC;
可选值包括DYNAMIC、COMPACT和REDUNDANT。这里COMPRESSED不能被设置为默认行记录格式,因为COMPRESSED
模式不能使用在系统表空间上。
除了使用默认行记录格式,当然可以在创建表的时候显示的指定行记录格式
CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;
查看当前表使用的行记录格式可以使用show table status like ‘表名’。
mysql> show table status like 'test'\G;
*************************** 1. row ***************************
Name: test
Engine: InnoDB
Version: 10
Row_format: Compact
...
或者可以从INFORMATION_SCHEMA.INNODB_TABLES表中查看。
实例分析
准备表
CREATE TABLE test (
id INT(11) NOT NULL AUTO_INCREMENT,
a VARCHAR(10) NOT NULL ,
b VARCHAR(30) NULL DEFAULT NULL ,
c TEXT NULL DEFAULT NULL ,
PRIMARY KEY (id) USING BTREE
)ENGINE=InnoDB
先插入一条记录
INSERT INTO test (a, b, c) VALUES ('a', 'b', null);
INSERT INTO test (a, b, c) VALUES ('a', 'bb', null);
找到表对应的数据文件test.ibd,使用editplus十六进制格式查看,查找62,因为’a’的ascii码也就是16进制格式是62。
80 00 00 02 00 00 00 00 93 EA B0 00 00 01 24 01
10 61 62 02 01 02 00 00 20 FF B5 80 00 00 02 00
00 00 00 93 EF 33 00 00 01 D1 02 41 61 62 62 00
这里很明显能看到 61 62 这些是实际数据ab,那从他这里往后应该就是第二行数据了。然后61 62 62这个是第二行的实际数据,那么这种中间的就是第二行的标识位信息。
结合第二行数据:2|a|bb|null
1、可变长度列表(逆序):02 01 (代表两个有值可变长度列长度第一个是1,第二个长度是2)
2、NULL标识位:02 。这里表最后两列是可空字段,然后一列有值一列没有值。02二进制格式是00000010。这里看到也是逆序标识的。
3、5字节的header头:00 00 20 FF B5
4、4字节的rowID(主键):80 00 00 02。这里主键设置int自增占4字节,实际值是2,不知道第一字节80代表什么。
5、剩下两部分TRX_ID和roll_pointer:00 00 00 00 93 EF 33 00 00 01 D1 02 41。
一个6字节一个7字节长度,共13字节
6、数据部分:61 62 62 (abb的ascii码)