MySQL存储引擎-InnoDB行格式
mysql作为一款主流的关系型数据库,是以记录为单位向表中插入数据的。目前为止,Innodb共支持COMPACT、 REDUNDANT、 DYNAMIC、和COMMPRESSED四种行格式。在MySQL5.7及以上版本,默认采用DYNAMIC格式。DYNAMIC与COMPACT格式基本一致,下文中我们会介绍区别。因其他类型日常工作中很少用到,本文作为读书笔记,不再进行介绍。
一、COMPACT行格式
我们首先创建一个示例表,如下:
create table record_format_demo ( c1 varchar(10), c2 varchar(10) not null, c3 char(10), c4 varchar(10) ) charset = ascii row_format = compact; insert into record_format_demo values('aaaa','bbb','cc','d'); insert into record_format_demo values('eeee','fff',NULL,NULL);
一条完整的记录被分为记录的额外信息和记录的真实数据两部分。
1.1、记录的额外信息
记录的额外信息分为3部分,分别是变长字段长度列表, NULL值列表,和记录头信息。
变长列表指的是varchar等不固定的长度类型,变长字段真实数据占用的字节数存储在记录的开头位置。形成一个变长字段长度列表,变长字段的真实数据所占用的字节数按照列的顺序逆序排放。例如示例表中的第一行数据。该示例表变长字段为c1、c2、c4列。因为我们使用的是ascii字符集,所以每个字符占用1字节。 因此:第一行数据的变长字段长度列表用十六进制表示为 01 03 04。 这个示例中的长度都可以用1字节表示,那如果变成字段的真实内容占用字节数很多,可能就用2字节来表示。InnoDB的规则是:如果变成字段允许存储的最大字节数小于255就用1个字节表示,如果大于255并且真实数据占用的字节数超过127字节就使用2字节来表示,不超过177就使用一个字节表示。 我们再看示例表的第二行,因为c4列为null。变成字段长度列表不存储NULL列的内容长度,因此第二行的变长列表为 03 04。
NULL值列表,在COMPACT中,把一条记录中为NULL的列统一管理,存储到NULL值列中。处理过程如下:首先统计允许存储NULL的列有哪些,例如示例表中的c1、c3、c4。如果表的所有列都不会NULL, 那NULL值列表就不存在了。如果有NULL值,就按照允许存储NULL的列对应一个二进制位,二进制为按照列的顺序逆序排列。二进制位为1时代表该列的值为NULL,为0时代表该列不为NULL。另外MySQL的NULL值列表必须用整数个字节表示,不足的在最高位补0。根据这个规则我们可以推算示例表中第一行数据的NULL值列表为 0 0 0 0 0 0 0 0 。第二行的NULL值列列表为 0 0 0 0 0 1 1 0。 转换为十六进制则为 00 和 06。
记录头信息,记录头信息固定5个字节,用于描述记录的一些属性。5个字节即40位,不同位代表不用的函数。二进制位代表的含义如下所示:
1、预留位 1位
2、预留位 1位
3、delete_flag 1位 标记是否删除
4、n_owned 4位 一个页面的记录会被分为若干组。每个组中有一个记录是大哥,其余为小弟。大哥的n_owned代表改组的记录条数,小弟的n_owned为0
5、heap_no 13位 表示当前记录在页面堆的相对位置
6、record_no 3位 表示记录类型。 0表示普通记录,1表示非叶结点记录,2表示Infimum记录, 3表示Supermum记录
7、next_record 16位 表示下一条记录的相对位置
1.2、记录的真实数据
MySQL的除了我们插入的真实数据之外,还会插入一些隐藏列。包括 rowid (6字节,在没有主键和非空唯一索引会创建)、trx_id (6字节,事务id)、roll_pointer (7字节,回滚指针)。根据以上我们的分析,我们通过hexdump指令来分析一下示例表的数据文件。结果如下:这里只截了一部分,即包含行数据的部分。
因此我们可以得到前两行的行记录为:
说明 | 变长字段 | NULL值列表 | 记录头信息 | row_id | trx_id | roll_pointer | c1列 | c2列 | c3列 | c4列 |
第一行 | 01 03 04 | 00 | 00 00 10 00 2d | 00 00 03 67 c5 78 | 00 00 13 82 3e 9e | a6 00 00 c0 2c 01 10 | 61 61 61 61 | 62 62 62 | 63 63 20 20 20 20 20 20 20 20 | 64 |
第二行 | 03 04 | 06 | 00 00 18 ff c2 | 00 00 03 67 c5 79 | 00 00 13 82 3e 9f | a7 00 00 01 ca 01 10 | 65 65 65 65 | 66 66 66 |
1.3、char列存储格式
在上述示例表中,我们采用的ascii字符集属于定长字符集即1个字节。如果使用变长字符集如utf8,utf8表示一个字符需要1~3个字节。针对变长字符集,虽然c3列的类型为char(10)。这一列的值占用的字节数也会被存储到变长字段长度列表中。执行如下sql修改:
alter table record_format_demo modify column c3 char(10) character set utf8;
那该列也会被加入变长列表:如图所示: 第一行的变长字段的长度列表变为了 01 0a 03 04
1.4、溢出列
我们创建一个溢出列的测试表,如下所示:
create table off_page_demo(c varchar(65532)) charset=ascii row_format=compact; insert into off_page_demo values(repeat('a', 65532));
上述表中,这个字符串大小为65532字节。我们知道MySQL一个数据页才16K,即16382字节,很显然1个数据页中存不了一条记录。 在COMPACT格式中:对于占用存储空间非常多的列,在记录的真实数据处只存储一部分数据,而把剩余的数据分散存储在其他页中。然后在记录真实数据处用20字节存储指向这些页的地址。(这20字节还包括分散在其他页中的数据所占的字节数)。 对于COMPACT格式,如果某些数据很多,则在本记录的真实数据处只存放768个字节和指向其他页的地址。 存储剩余数据的页叫做溢出页。
MySQL规定一个页至少存储2条记录。对于溢出页没有这个规定, 可以使用如下命令: hexdump -C -v off_page_demo.ibd 。因图太大,这里我们分多张展示关键部分
根据图我们也可以计算出真是数据存储 a的数量是 768个。 再往后数20个字节存的是溢出页的地址(包括这些溢出页所占用的真实字节数),其他用于存放剩余数据的页用链表连接起来的。
变长字段 | NULL值列表 | 记录头信息 | row_id | trx_id | roll_pointer | c1列 |
14 c3 | 00 | 00 00 10 ff f0 | 00 00 03 67 c5 7c | 00 00 13 82 3e c3 | be 00 00 80 3f 01 10 | 61 61......61 |
二、其他格式
另外两个行格式,DYNAMIC格式和COMPRESSED格式。这另个行格式和COMPACT格式很像,只不过在处理溢出列时有些分歧。他们不会在记录真实数据处存储前768字节,而是把该列的所有真实数据全部存放到溢出页中。只在记录真实数据处存储20字节大小的指向溢出页的地址,COMPRESSED会采用压缩算法对页面进行压缩,基本用不到,就不再介绍了。
这里我们再验证一下DYNAMIC格式的溢出页。
关于DYNAMIC格式的普通页读者可自行验证。
三、行格式图展示