后面也会持续更新,学到新东西会在其中补充。
建议按顺序食用,欢迎批评或者交流!
缺什么东西欢迎评论!我都会及时修改的!
MySQL原理 - InnoDB引擎 - 行记录存储 - Compact 行格式 - 知乎
MySQL原理 - InnoDB引擎 - 行记录存储 - Redundant行格式 - 知乎
MySQL原理 - InnoDB引擎 - 行记录存储 - Off-page 列 - 知乎
大部分截图采用该书,谢谢这位大佬的文章,在这里真的很感谢让迷茫的我找到了很好的学习文章。我只是加上了自己的拙见 。我只是记录学习没有任何抄袭意思
MySQL 是怎样运行的:从根儿上理解 MySQL - 小孩子4919 - 掘金小册
实验环境
MySQL9.04
InnoDB页
将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。
InnoDB行格式
InnoDB
存储引擎4种不同类型的**行格式
,分别是Compact
、Redundant
、Dynamic
** 和**Compressed
**行格式。
环境搭建
sql
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
sql
#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);
sql
mysql> show variables like '%datadir%';
+---------------+----------------------------+
| Variable_name | Value |
+---------------+----------------------------+
| datadir | /soft/mysql/mysql9.0/data/ |
+---------------+----------------------------+
1 row in set (0.00 sec)
bash
#后面的二进制文件我都是这样生成的!有些地方我就不贴重复代码了。(太懒了)
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
类型,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据 的时候需要顺便把这些数据占用的字节数也存起来。
变长字段占用的存储空间分为两部分:
- 真正的数据内容
- 占用的字节数
上图看不明白很正常后面会解释!
在**Compact
** 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表 ,各变长字段数据占用的字节数按照列的顺序逆序存放。
为什么要逆序存放?
记录头信息 中指向下一个记录的指针( next_record**)** ,指向的是下一条记录的记录头信息 和真实数据之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据。
同时,使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。
record_format_demo
表的c1
、c2
、c4
列都是VARCHAR(10)
类型的,所以这三个列的值的长度都需要保存在记录开头处 ,因为record_format_demo
表中的各个列都使用的是ascii字符集编码,所以每个字符只需要1个字节来进行编码。
列名 | 存储内容 | 内容长度(十进制表示) | 内容长度(十六进制表示) |
---|---|---|---|
c1 |
'aaaa' |
4 |
0x04 |
c2 |
'bbb' |
3 |
0x03 |
c4 |
'd' |
1 |
0x01 |
sql
#逆序排序
01 03 04 这个称之为变长字段长度列表
将变长字段长度列表插入
由于第一行记录中c1
、c2
、c4
列中的字符串都比较短,也就是说内容 占用的字节数比较小,用1个字节就可以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。具体用1个还是2个字节来表示真实数据占用的字节数。
因此InnoDB 有它的一套规则**W
、M
和L
**
某个字符集中表示一个字符最多需要使用的字节数为**W
** 也就是使用**SHOW CHARSET
** 语句的结果中的**Maxlen
** 列,比方说**utf8
** 字符集中的W
就是3
,gbk
字符集中的W
就是2
,ascii
字符集中的**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个字节来表示真正字符串占用的字节数。
sql
#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
sql
#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
sql
#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。
sql
#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
,所以第二条记录的变长字段长度列表
只需要存储c1
和c2
列的长度即可。其中c1
列存储的值为'eeee'
,占用的字节数为4
,c2
列存储的值为'fff'
,占用的字节数为3
。数字4
可以用1个字节表示,3
也可以用1个字节表示,所以整个变长字段长度列表
共需2个字节。填充完变长字段长度列表
**的两条记录的对比图如下:
并不是所有记录都有这个变长字段长度列表 部分,比方说表中所有的列都不是变长的数据类型的话,这一部分就不需要有。
varchar(n) 中 n 最大取值为多少?
MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节 。服务器建议我们把存储类型改为TEXT
或者BLOB
的类型。
sql
CREATE TABLE test (
`name` VARCHAR(65535) NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;
storage overhead 是变长字段长度列表和 NULL 值列表。
varchar(n) 的数据分成了三个部分来存储
- 真实数据
- 真实数据占用的字节数
- NULL 标识,如果该列有
NOT NULL
属性则可以没有这部分存储空间
单列数据
sql
# 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;
不同字符集编码
sql
# 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;
多列数据
sql
# 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个列c1
、c3
、c4
都是允许存储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) | 描述 |
---|---|---|
预留位1 |
1 |
没有使用 |
预留位2 |
1 |
没有使用 |
delete_mask |
1 |
标记该记录是否被删除 标识此条数据是否被删除。执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 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 |
表示下一条记录的相对位置 记录与记录之间是通过链表组织的。 |
两条记录的头信息
第一行记录:
记录的真实数据
MySQL
会为每个记录默认的添加一些列(也称为隐藏列
),具体的列如下:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id |
否 | 6 字节 |
行ID,唯一标识一条记录 |
transaction_id |
是 | 6 字节 |
事务ID |
roll_pointer |
是 | 7 字节 |
回滚指针 |
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条记录中c3
和c4
列的值都为NULL
,它们被存储在了前面的NULL值列表
处,在记录的真实数据处就不再冗余存储,从而节省存储空间。
CHAR(M)列的存储格式
record_format_demo
表中的**c3
列的类型是CHAR(10)。
**
record_format_demo
表采用的是**ascii
字符集,这个字符集是一个定长字符集,也就是说表示一个字符采用固定的一个字节。**
如果采用变长的字符集(也就是表示一个字符需要的字节数不确定,比如gbk
表示一个字符要1~2个字节、utf8
表示一个字符要1~3个字节等)的话。
c3
列的长度也会被存储到**变长字段长度列表
**中
sql
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个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。
sql
#不要一次执行完!
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
sql
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');
更新前第一行和第二行数据
更新后第一行和第二行数据
sql
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;
sql
update `record_test_1` set `large_content` = '' where id = 1;
sql
mysql> delete from record_test_1 where id = 1;
Query OK, 1 row affected (0.00 sec)
行溢出数据
记录中的数据太多产生的溢出
sql
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
**个字节,这样就可能造成一个页存放不了一条记录的尴尬情况。
在**Compact
** 和**Redundant
** 行格式中,对于占用存储空间非常大的列,在**记录的真实数据
处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据
**处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。
对于**Compact
和Redundant
行格式来说,如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前768
个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出
,存储超出768
字节的那些页面也被称为溢出页
**。
不只是 VARCHAR(M) 类型的列,其他的 TEXT 、BLOB 类型的列在存储数据非常多的时候也会发生行溢出
。
行溢出的临界点
MySQL
中规定一个页中至少存放两行记录
页中的空间都是如何利用的:
compact格式:
-
每个页除了存放我们的记录以外,也需要存储一些额外的信息,乱七八糟的额外信息加起来需要**
132
个字节**的空间(现在只要知道这个数字就好了),其他的空间都可以被用来存储记录。 -
每个记录需要的额外信息是**
27
字节**。 -
2个字节用于存储真实数据的长度
-
6个字节大小的头信息
-
6个字节的
row_id
列 -
6个字节的
transaction_id
列 -
7个字节的
roll_pointer
列
如果该列不发生溢出的现象,就需要满足下边这个式子:
sql
132(页额外)+ 2(一页两条记录)×(27(记录额外) + n(单列数据字节数)) < 16384
compact:n < 8099
。也就是说如果一个列中存储的数据小于8099
个字节,那么该列就不会成为**溢出列
,否则该列就需要成为溢出列
**。
注意这里说的单列不是多列哦。
如果我们一条记录的某个列中存储的数据占用的字节数非常多时,该列就可能成为
溢出列
。
Dynamic和Compressed行格式
这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
Redundant行格式
**Redundant
**行格式的全貌
sql
#表恢复到环境搭建状态
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。
所有字段倒序排列
sql
#逆序排序
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) | 描述 |
---|---|---|
预留位1 |
1 |
没有使用 |
预留位2 |
1 |
没有使用 |
delete_mask |
1 |
标记该记录是否被删除 |
min_rec_mask |
1 |
B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 |
表示当前记录拥有的记录数 |
heap_no |
13 |
表示当前记录在页面堆的位置信息 |
n_field |
10 |
表示记录中列的数量 |
1byte_offs_flag |
1 |
标记字段长度偏移列表中每个列对应的偏移量是使用1字节还是2字节表示的 |
next_record |
16 |
表示下一条记录的相对位置 |
第一行记录的头信息
sql
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_field
和1byte_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个字节。
sql
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 这样
第二条字段偏移列表
sql
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)
类型的列是不会产生碎片的。
sql
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
sql
#不要一次执行完!
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 字节会被存储在数据行上,剩下的数据会被放入其他页。
sql
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字节吧)
感受
能看到这里的你们真的很厉害了,我看这个东西都放弃了好几次,都有点自我怀疑了。感谢我的朋友指正我让我坚持下去了。
总结
所有结论都需要反复测试!如果有错误欢迎指正!一起努力!
如果喜欢的话,请点个赞吧就算鼓励我一下