Bootstrap

MySQL(行结构)

后面也会持续更新,学到新东西会在其中补充。

建议按顺序食用,欢迎批评或者交流!

缺什么东西欢迎评论!我都会及时修改的!

MySQL 一行记录是怎么存储的? | 小林coding

MySQL原理 - InnoDB引擎 - 行记录存储 - Compact 行格式 - 知乎

MySQL原理 - InnoDB引擎 - 行记录存储 - Redundant行格式 - 知乎

MySQL原理 - InnoDB引擎 - 行记录存储 - Off-page 列 - 知乎

大部分截图采用该书,谢谢这位大佬的文章,在这里真的很感谢让迷茫的我找到了很好的学习文章。我只是加上了自己的拙见我只是记录学习没有任何抄袭意思

MySQL 是怎样运行的:从根儿上理解 MySQL - 小孩子4919 - 掘金小册

实验环境

MySQL9.04 

InnoDB页

将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB

InnoDB行格式

InnoDB存储引擎4种不同类型的行格式,分别是CompactRedundantDynamicCompressed行格式。

环境搭建

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
    
ALTER TABLE 表名 ROW_FORMAT=行格式名称
#ascii字符集只包括空格、标点符号、数字、大小写字母和一些不可见字符。
#所以我们的汉字是不能存到这个表里的。
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(c1, c2, c3, c4) VALUES('aaaa', 'bbb', 'cc', 'd'), ('eeee', 'fff', NULL, NULL);
mysql> show variables like '%datadir%';
+---------------+----------------------------+
| Variable_name | Value                      |
+---------------+----------------------------+
| datadir       | /soft/mysql/mysql9.0/data/ |
+---------------+----------------------------+
1 row in set (0.00 sec)

#后面的二进制文件我都是这样生成的!有些地方我就不贴重复代码了。(太懒了)
hexdump -C -v record_format_demo.ibd > /root/record_format_demo.txt

 ctrl + F 搜索 supremum,我是用记事本打开的(嘿嘿)。

COMPACT格式

 一条完整的记录其实可以被分为记录的额外信息记录的真实数据两大部分。

记录的额外信息

这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是变长字段长度列表NULL值列表记录头信息。

变长字段长度列表

MySQL支持的一些变长类型比如VARCHAR(M)VARBINARY(M)、各种TEXT类型,各种BLOB类型,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。

变长字段占用的存储空间分为两部分:

  1. 真正的数据内容
  2. 占用的字节数

上图看不明白很正常后面会解释!

Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放。

为什么要逆序存放?

记录头信息中指向下一个记录的指针(next_record,指向的是下一条记录的记录头信息真实数据之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据。

同时,使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率

record_format_demo表的c1c2c4列都是VARCHAR(10)类型的,所以这三个列的值的长度都需要保存在记录开头处,因为record_format_demo表中的各个列都使用的是ascii字符集编码,所以每个字符只需要1个字节来进行编码。

列名存储内容内容长度(十进制表示)内容长度(十六进制表示)
c1'aaaa'40x04
c2'bbb'30x03
c4'd'10x01
#逆序排序
01 03 04 这个称之为变长字段长度列表

将变长字段长度列表插入

由于第一行记录中c1c2c4列中的字符串都比较短,也就是说内容占用的字节数比较小,用1个字节就可以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。具体用1个还是2个字节来表示真实数据占用的字节数。

 因此InnoDB有它的一套规则WML

某个字符集中表示一个字符最多需要使用的字节数为W也就是使用SHOW CHARSET语句的结果中的Maxlen列,比方说utf8字符集中的W就是3gbk字符集中的W就是2ascii字符集中的W就是1

对于变长类型VARCHAR(M)来说,这种类型表示能存储最多M个字符(注意是字符不是字节比如a就算一个字符而不是一个字节),所以这个类型能表示的字符串最多占用的字节数就是M×W

它实际存储的字符串占用的字节数L (实际上我也不太理解L到底是啥,但不影响理解。创建表好像和L也没关系我后面实验了一下!)

所以确定使用1个字节还是2个字节表示真正字符串占用的字节数的规则就是这样:

如果M×W <= 255,那么使用1个字节来表示真正字符串占用的字节数。

InnoDB在读记录的变长字段长度列表时先查看表结构,如果某个变长字段允许存储的最大字节数不大于255时,可以认为只使用1个字节来表示真正字符串占用的字节数。

如果M×W > 255,则分为两种情况:

  • 如果L <= 127,则用1个字节来表示真正字符串占用的字节数。
  • 如果L > 127,则用2个字节来表示真正字符串占用的字节数。
#200 * 1 <= 255 1个字节
CREATE TABLE test ( 
`name` VARCHAR(200)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

INSERT INTO test VALUES(REPEAT('a', 128));

hexdump -C -v test.ibd > /root/test.ibd.txt

#255 * 1 > 255 2个字节
CREATE TABLE test ( 
`name` VARCHAR(255)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

INSERT INTO test VALUES(REPEAT('a', 128));

hexdump -C -v test.ibd > /root/test.ibd.txt

#255 * 1 > 255 2个字节
CREATE TABLE test ( 
`name` VARCHAR(256)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

INSERT INTO test VALUES(REPEAT('a', 256));

hexdump -C -v test.ibd > /root/test.ibd.txt

 81?对了我们要从右往左读,也就是常说的低位在左高位在右。8100

 1000 0001 0000 0000 

 1(这个1是标识代表了这个字符串有2个字节)000 0001 0000 0000 

0000 0001 0000 0000 实际上就是256 也就是2的8次方。

那么为什么用 128 作为分界线呢? 

一个字节可以最多表示255

但是 MySQL 设计长度表示时,为了区分是否是一个字节表示长度,规定,如果最高位为1,那么就是两个字节表示长度,否则就是一个字节。

例如,01111111,这个就代表长度为 127,而如果长度是 128,就需要两个字节,就是 10000000 10000000。

首个字节的最高位为1。那么这就是两个字节表示长度的开头,第二个字节可以用所有位表示长度,并且需要注意的是,MySQL采取 Little Endian 的计数方式,低位在前,高位在后,所以 129 就是 10000001 10000000。

#255 * 1 > 255 2个字节
CREATE TABLE test ( 
`name` VARCHAR(256)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

INSERT INTO test VALUES(REPEAT('a', 129));

hexdump -C -v test.ibd > /root/test.ibd.txt

 同时,这种标识方式,最大长度就是 2^15 - 1 = 32767,也就是32 KB。

InnoDB在读记录的 变长字段长度列表时先查看 表结构,如果 某个变长字段允许存储的 最大字节数(M x W)不大于255时,可以认为只使用1个字节来表示真正字符串占用的字节数。
总结一下就是说:如果该可变字段允许存储的 最大字节数M×W)超过255字节并且 真实存储的字节数L)超过127字节,则使用2个字节,否则使用1个字节。

另外需要注意的一点是,变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的 。也就是说对于第二条记录来说,因为c4列的值为NULL,所以第二条记录的变长字段长度列表只需要存储c1c2列的长度即可。其中c1列存储的值为'eeee',占用的字节数为4c2列存储的值为'fff',占用的字节数为3。数字4可以用1个字节表示,3也可以用1个字节表示,所以整个变长字段长度列表共需2个字节。填充完变长字段长度列表的两条记录的对比图如下:

并不是所有记录都有这个 变长字段长度列表 部分,比方说表中所有的列都不是变长的数据类型的话,这一部分就不需要有。

varchar(n) 中 n 最大取值为多少?

MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节服务器建议我们把存储类型改为TEXT或者BLOB的类型。

CREATE TABLE test ( 
`name` VARCHAR(65535)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

storage overhead 变长字段长度列表NULL 值列表。 

varchar(n) 的数据分成了三个部分来存储

  • 真实数据
  • 真实数据占用的字节数
  • NULL 标识,如果该列有NOT NULL属性则可以没有这部分存储空间

单列数据

# 65534 + 2 + 0 (not null) > 65535 失败
CREATE TABLE test ( 
`name` VARCHAR(65534)  NOT NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

# 65533 + 2 + 0 (not null) <= 65535 成功
CREATE TABLE test ( 
`name` VARCHAR(65533)  NOT NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

# 65533 + 2 + 1 (null) > 65535     失败
CREATE TABLE test ( 
`name` VARCHAR(65533)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

# 65532 + 2 + 1 (null) <= 65535     成功
CREATE TABLE test ( 
`name` VARCHAR(65532)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

 不同字符集编码

# 65532 + 2 + 1 (null) <= 65535     成功
CREATE TABLE test ( 
`name` VARCHAR(65532)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

mysql> CREATE TABLE test (
    -> `name` VARCHAR(65532)  NULL
    -> ) ENGINE = InnoDB DEFAULT CHARACTER SET = gbk ROW_FORMAT = COMPACT;
ERROR 1074 (42000): Column length too big for column 'name' (max = 32767); use BLOB or TEXT instead

# 65532 / 2 = 32766 + 2 + 1(null) <= 65535   成功
CREATE TABLE test ( 
`name` VARCHAR(32766)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = gbk ROW_FORMAT = COMPACT;

mysql> CREATE TABLE test (
    -> `name` VARCHAR(32766)  NULL
    -> ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 ROW_FORMAT = COMPACT;
ERROR 1074 (42000): Column length too big for column 'name' (max = 16383); use BLOB or TEXT instead

# 65532 / 4 = 16383 + 2 + 1(null) <= 65535   成功
CREATE TABLE test ( 
`name` VARCHAR(16383)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 ROW_FORMAT = COMPACT;

多列数据

# name [65530 + 2 + 0 (not null) ] + age[2 + 1 + 0(not null)] <= 65535 成功
CREATE TABLE test ( 
`name` VARCHAR(65530)  NOT NULL,
`age` VARCHAR(2)  NOT NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

# name [65270 + 2 + 0 (not null) ] + age[256 + 2 + 0(not null)] <= 65535 成功
CREATE TABLE test ( 
`name` VARCHAR(65270)  NOT NULL,
`age` VARCHAR(256)  NOT NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

# name [65276 + 2 + 0 (not null) ] + age[256 + 2 + 0(not null)] >= 65535 失败
CREATE TABLE test ( 
`name` VARCHAR(65276)  NOT NULL,
`age` VARCHAR(256)  NOT NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

# name [65276 + 2 + 0 (not null) ] + age[256 + 2 + 0(not null)] >= 65535 失败
CREATE TABLE test ( 
`name` VARCHAR(65276)  NOT NULL,
`age` VARCHAR(256)  NOT NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

总结一下:

server层通用处理规则

M x N <= 255时,只需要一个字节来表示占用的字节数。

M x N > 255 且 M x W < 65536时,需要两个字节来表示占用的字节数

innoDB的存储规则

如果M×W > 255,则分为两种情况:

  • 如果L <= 127,则用1个字节来表示真正字符串占用的字节数。
  • 如果L > 127,则用2个字节来表示真正字符串占用的字节数。

这个是两个判断,第一个判断是判断字段最大长度M*编码占用字节数N是否大于 255,如果符合,就只用一个字节保存,最高位也参与存储。注意这里不是实际占用,而是通过最大可能占用判断的。
否则,就第一个字节最高位不参与存储,而是作为标记,标记占用一个字节还是两个字节。
比如 varchar(60) utf8,用一个字节存储,最高位参与存储。varchar(128) utf8 最高位就不参与存储,如果是存3个字符,就用一个字节,存 60 个字符就用 两个字节。

 NULL值列表

采用 BitMap 的思想,标记这些字段,可以节省空间。Null值列表就是这样的一个 BitMap。

如果把这些NULL值都放到记录的真实数据中存储会很占地方,所以Compact行格式把这些值为NULL的列统一管理起来。

首先统计表中允许存储NULL的列有哪些。

主键列、被NOT NULL修饰的列都是不可以存储NULL值的,在统计的时候不会把这些列算进去。

 比方说表record_format_demo的3个列c1c3c4都是允许存储NULL值的,而c2列是被NOT NULL修饰,不允许存储NULL值。

如果表中没有允许存储 NULL 的列,则 NULL值列表 也不存在了。否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列

  • 二进制位的值为1时,代表该列的值为NULL

  • 二进制位的值为0时,代表该列的值不为NULL

MySQL规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0

如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了。(一个字节8位 9个代表着有9位了)

 

 第一条记录的null值列表用十六进制表示就是:0x00 

 第二条记录的null值列表用十六进制表示就是:0x06 (0000 0110)

所以这两条记录在填充了NULL值列表后的示意图就是这样: 

记录头信息

由固定的5个字节组成。

名称大小(单位:bit)描述
预留位11没有使用
预留位21没有使用
delete_mask1

标记该记录是否被删除

标识此条数据是否被删除。执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。

min_rec_mask1B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned4表示当前记录拥有的记录数
heap_no13表示当前记录在记录堆的位置信息
record_type3

表示当前记录的类型,

0表示普通记录,

1表示B+树非叶子节点记录,

2表示最小记录,

3表示最大记录

next_record16

表示下一条记录的相对位置

记录与记录之间是通过链表组织的。

两条记录的头信息 

第一行记录:

记录的真实数据

MySQL会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下: 

列名是否必须占用空间描述
row_id6字节行ID,唯一标识一条记录
transaction_id6字节事务ID
roll_pointer7字节回滚指针

 InnoDB表主键生成策略:

自定义主键 -> 唯一字段 -> rowid列

InnoDB存储引擎会为每条记录都添加 transaction_id 和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。这些隐藏列的值不用我们操心,InnoDB存储引擎会自己帮我们生成的。

因为record_format_demo表没有定义主键、也没有唯一字段。因此会创建rowid列。

 第一条记录:

record_format_demo使用的是ascii字符集,所以0x61616161(c1列的值)就表示字符串'aaaa'0x626262(c2列的值)就表示字符串'bbb',以此类推。

 注意第1条记录中c3列的值,它是CHAR(10)类型的,它实际存储的字符串是:'cc',而ascii字符集中的字节表示是'0x6363',虽然表示这个字符串只占用了2个字节,但整个c3列仍然占用了10个字节的空间,除真实数据以外的8个字节的统统都用空格字符填充,空格字符在ascii字符集的表示就是0x20

 注意第2条记录中c3c4列的值都为NULL,它们被存储在了前面的NULL值列表处,在记录的真实数据处就不再冗余存储,从而节省存储空间。

CHAR(M)列的存储格式

record_format_demo表中的c3列的类型是CHAR(10)

record_format_demo表采用的是ascii字符集,这个字符集是一个定长字符集,也就是说表示一个字符采用固定的一个字节。

如果采用变长的字符集(也就是表示一个字符需要的字节数不确定,比如gbk表示一个字符要1~2个字节、utf8表示一个字符要1~3个字节等)的话。

c3列的长度也会被存储到变长字段长度列表

mysql> alter table record_format_demo modify column c3 char(10) character set utf8mb4;
Query OK, 2 rows affected (0.05 sec)
Records: 2  Duplicates: 0  Warnings: 0

对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。

长字符集的CHAR(M)类型的列要求至少占用M个字节,而VARCHAR(M)却没有这个要求。

比方说对于使用utf8字符集的CHAR(10)的列来说,该列存储的数据字节长度的范围是10~30个字节。

即使我们向该列中存储一个空字符串也会占用10个字节,这是怕将来更新该列的值的字节长度大于原有值的字节长度而小于10个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。 

#不要一次执行完!
mysql> update record_format_demo set c3 = 'cccccccccc';
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

mysql> update record_format_demo set c3 = '王ccccccccc';
Query OK, 2 rows affected (0.01 sec)
Rows matched: 2  Changed: 2  Warnings: 0

create table record_test_1 (
	id bigint,
	score double,
	name char(4),
	content varchar(8),
	extra varchar(16)
)CHARSET=ascii row_format=compact;

INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (1, 78.5, 'hash', 'wodetian', 'nidetiantadetian');
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (65536, 17983.9812, 'zhx', 'shin', 'nosuke');
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (NULL, -669.996, 'aa', NULL, NULL);
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (2048, NULL, NULL, 'c', 'jun');

更新前第一行和第二行数据 

 更新后第一行和第二行数据 

alter table `record_test_1` 
add column `large_content` varchar(1024) null after `extra`;

update `record_test_1` set `large_content` = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz' where id = 1;

update `record_test_1` set `large_content` = '' where id = 1;

mysql> delete from record_test_1 where id = 1;
Query OK, 1 row affected (0.00 sec)

行溢出数据

记录中的数据太多产生的溢出

CREATE TABLE test ( 
`name` VARCHAR(65532)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

INSERT INTO test VALUES(REPEAT('a', 65532));
REPEAT('a', 65532)是一个函数调用,它表示生成一个把字符'a'重复65532次的字符串。

MySQL中磁盘和内存交互的基本单位是,而一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65532个字节,这样就可能造成一个页存放不了一条记录的尴尬情况。

CompactRedundant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。

 对于CompactRedundant行格式来说,如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前768个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页

不只是 VARCHAR(M) 类型的列,其他的 TEXTBLOB 类型的列在存储数据非常多的时候也会发生行溢出

行溢出的临界点

MySQL中规定一个页中至少存放两行记录

页中的空间都是如何利用的:

compact格式: 

  • 每个页除了存放我们的记录以外,也需要存储一些额外的信息,乱七八糟的额外信息加起来需要132个字节的空间(现在只要知道这个数字就好了),其他的空间都可以被用来存储记录。

  • 每个记录需要的额外信息是27字节

  • 2个字节用于存储真实数据的长度
  • 6个字节大小的头信息
  • 6个字节的row_id
  • 6个字节的transaction_id
  • 7个字节的roll_pointer

如果该列不发生溢出的现象,就需要满足下边这个式子:

132(页额外)+ 2(一页两条记录)×(27(记录额外) + n(单列数据字节数)) < 16384   

compact:n < 8099。也就是说如果一个列中存储的数据小于8099个字节,那么该列就不会成为溢出列,否则该列就需要成为溢出列。 

注意这里说的单列不是多列哦。

如果我们一条记录的某个列中存储的数据占用的字节数非常多时,该列就可能成为溢出列

Dynamic和Compressed行格式

这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。 

Redundant行格式

Redundant行格式的全貌

#表恢复到环境搭建状态
mysql> ALTER TABLE record_format_demo ROW_FORMAT=Redundant;
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

两行数据是这样的:

字段长度偏移列表

注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:

  • 没有了变长两个字,意味着Redundant行格式会把该条记录中所有列(包括隐藏列的长度信息都按照逆序存储到字段长度偏移列表

  • 多了个偏移两个字,这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。

记录所有字段的长度偏移,包括隐藏列。偏移就是,第一个字段长度为 a,第二个字段长度为b,那么列表中第一个字段就是 a,第二个字段就是 a + b。

所有字段倒序排列

#逆序排序
25 24 1A 17 13 0C 06

#为了方便变成顺序
06 0C 13 17 1A 24 25

第一列(`row_id`)的偏移长度就是 0x06个字节,也就是6个字节。

第二列(`transaction_id`)的偏移长度就是 0x0C (0x06 + 0x06)个字节

第三列(`roll_pointer`)的偏移长度就是 0x13 (0x0C + 0x07)个字节,也就是7个字节。

第四列(`c1`)的偏移长度就是 0x17 (0x013 + 0x04)个字节,也就是4个字节(aaaa)。

 记录头信息

Redundant行格式的记录头信息占用6字节,48个二进制位

名称大小(单位:bit)描述
预留位11没有使用
预留位21没有使用
delete_mask1标记该记录是否被删除
min_rec_mask1B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned4表示当前记录拥有的记录数
heap_no13表示当前记录在页面堆的位置信息
n_field10表示记录中列的数量
1byte_offs_flag1标记字段长度偏移列表中每个列对应的偏移量是使用1字节还是2字节表示的
next_record16表示下一条记录的相对位置

第一行记录的头信息

00 00 10 0F 00 BC

预留位1:0x00
预留位2:0x00
delete_mask: 0x00
min_rec_mask: 0x00
n_owned: 0x00
heap_no: 0x02
n_field: 0x07
1byte_offs_flag: 0x01
next_record:0xBC

Compact行格式的记录头信息对比来看,有两处不同:

  • Redundant行格式多了n_field1byte_offs_flag这两个属性。

  • Redundant行格式没有record_type这个属性。

1byte_offs_flag的值是怎么选择的

字段长度偏移列表实质上是存储每个列中的值占用的空间在记录的真实数据处结束的位置,还是拿record_format_demo第一条记录为例,0x06代表第一个列在记录的真实数据第6个字节处结束,0x0C代表第二个列在记录的真实数据第12个字节处结束,0x13代表第三个列在记录的真实数据第19个字节处结束,等等等等,最后一个列对应的偏移量值为0x25,也就意味着最后一个列在记录的真实数据第37个字节处结束,也就意味着整条记录的真实数据实际上占用37个字节。

实际上就是每个列的长度加起来! 

 每个列对应的偏移量可以占用1个字节或者2个字节来存储,是根据该条Redundant行格式记录的真实数据占用的总大小。

  • 当记录的真实数据占用的字节数不大于127(十六进制0x7F,二进制01111111)时,每个列对应的偏移量占用1个字节

  • 当记录的真实数据占用的字节数大于127,但不大于32767(十六进制0x7FFF,二进制0111111111111111)时,每个列对应的偏移量占用2个字节

CREATE TABLE test ( 
`name` VARCHAR(255)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = redundant;

INSERT INTO test VALUES(REPEAT('a',127));

真实数据大于32767在本页中只保留前768个字节和20个字节的溢出页面地址(当然这20个字节中还记录了一些别的信息)。因为字段长度偏移列表处只需要记录每个列在本页面中的偏移就好了,所以每个列使用2个字节来存储偏移量就够了。

为了在解析记录时知道每个列的偏移量是使用1个字节还是2个字节表示的,设计Redundant行格式的大叔特意在记录头信息里放置了一个称之为1byte_offs_flag的属性:

  • 当它的值为1时,表明使用1个字节存储。

  • 当它的值为0时,表明使用2个字节存储。

 Redundant行格式中NULL值的处理

因为Redundant行格式并没有NULL值列表,所以设计Redundant行格式的大叔在字段长度偏移列表中的各个列对应的偏移量处做了一些特殊处理 —— 将列对应的偏移量值的第一个比特位作为是否为NULL的依据,该比特位也可以被称之为NULL比特位。也就是说在解析一条记录的某个列时,首先看一下该列对应的偏移量的NULL比特位是不是为1如果为1,那么该列的值就是NULL,否则不是NULL

还记得前文讲的低位在左,高位在右吗? 92 00 需要 00 92 这样读 0代表不为NULL。

这也就解释了上边介绍为什么只要记录的真实数据大于127(十六进制0x7F,二进制01111111)时,就采用2个字节来表示一个列对应的偏移量,主要是第一个比特位是所谓的NULL比特位,用来标记该列的值是否为NULL。 

1000 若不设置为2个字节的话 就代表着是NULL,但是实际上他是有值的,因此需要 0000 1000 这样

第二条字段偏移列表 

A4 A4 1A 17 13 0C 06
#顺序排一下好看!
06 0C 13 17 1A A4 A4

 需要注意的是c4的值为空

如果存储NULL的字段是定长类型的,比方说CHAR(M)数据类型的,则NULL也将占用记录的真实数据部分,并把该字段对应的数据使用0x00字节填充

c3列的值是NULL,c3列的类型是CHAR(10),占用记录的真实数据部分10字节,所以我们看到在Redundant行格式中使用0x00000000000000000000来表示NULL值。c3列占用的存储空间为10个字节。

compact是采用NULL列0 1表示的。

 如果该存储NULL值的字段是变长数据类型的,则不在记录的真实数据处占用任何存储空间。

c4列本身不占用任何记录的实际数据处的空间。

CHAR(M)列的存储格式

Compact行格式在CHAR(M)类型的列中存储数据的时候还挺麻烦,分变长字符集和定长字符集的情况,而在Redundant行格式中十分干脆,不管该列使用的字符集是啥,只要是使用CHAR(M)类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和M的乘积。比方说使用utf8字符集的CHAR(10)类型的列占用的真实数据空间始终为30个字节,使用gbk字符集的CHAR(10)类型的列占用的真实数据空间始终为20个字节。由此可以看出来,使用Redundant行格式的CHAR(M)类型的列是不会产生碎片的。

mysql> alter table record_format_demo modify column c3 char(10) character set utf8mb4;
Query OK, 2 rows affected (0.05 sec)
Records: 2  Duplicates: 0  Warnings: 0
#不要一次执行完!
mysql> update record_format_demo set c3 = 'cccccccccc';
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

mysql> update record_format_demo set c3 = '王ccccccccc';
Query OK, 2 rows affected (0.01 sec)
Rows matched: 2  Changed: 2  Warnings: 0

 Redundant 中 off-page 列处理

对于 Redundant 行格式中比较长的列,只有前 768 字节会被存储在数据行上,剩下的数据会被放入其他页。

drop table if exists long_column_test;
CREATE TABLE `long_column_test` (
`large_content` varchar(32768) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=REDUNDANT;

##长度为 768 字节
insert into long_column_test values (repeat("az", 384));
##长度为 8100 字节
insert into long_column_test values (repeat("az", 4050));
##长度为 32768 字节
insert into long_column_test values (repeat("az", 16384));

对于第二行,我们发现这一行的 large_content 列的数据并没有完全存储在这一行,而是一部分存储在这一行,另一部分存储在了其他地方,这种列就被称为 off-page 列,存储到的其他地方被称为 overflow 页,其结构如下:

 对于 off-page 列,列数据末尾会存在指向剩余数据所在地址的指针,这个指针占用 20 字节,它的结构是:(待续这里data length 是8 bytes 不确定我当前水平不够待续先认为是8字节吧)

感受

能看到这里的你们真的很厉害了,我看这个东西都放弃了好几次,都有点自我怀疑了。感谢我的朋友指正我让我坚持下去了。

总结

所有结论都需要反复测试!如果有错误欢迎指正!一起努力!

如果喜欢的话,请点个赞吧就算鼓励我一下

;