4、InnoDB
记录结构
声明:此博客是本人学习《MySQL是怎样运行的:从根儿上理解MySQL》的学习笔记,侵权必删。
注:由于内容是由markdown文件直接导入的,难免会有错误。如果你发现博客中有错误,还请告诉我,我会第一时间修改并给予您诚挚的感谢ღ( ´・ᴗ・` )。
InnoDB
和InnoDB
页
InnoDB
是存储引擎的一种,其将数据存储在磁盘中;InnoDB
页是在InnoDB
存储引擎中磁盘和内存交互的最小单元,大小为16KB
,类似于操作系统中的页。
InnoDB
行格式
我们平时是以记录为单位来向表中插入数据的,这些数据在磁盘的存储方式就被称为"行格式"或者"记录格式"。
在InnoDB
存储引擎中,行格式有四种:Compact
、Redundant
、Dynamic
和Compressed
。那么如何指定行格式呢?
我们可以在创建表或修改表结构时指定行格式:
# 创建表时
CREATE TABLE 表名 (列的信息...) ROW_FORMAT=行格式名称;
# 修改表时
ALTER TABLE 表名 ROW_FORMAT=行格式名称;
# 例子:创建一个test测试表,指定它的行格式为 Compact,设置字符集为 ASCII
CREATE TABLE test (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10),
c4 VARCHAR(10)
) CHARSET=ASCII ROW_FORMAT=COMPACT;
Compact
格式
可以看到Compact
格式的行格式包括额外信息和真实数据两部分,假设我们执行了如下语句:
INSERT INTO test (c1, c2, c3, c4)
VALUES ('aaaa', 'bbb', 'cc', 'd'),
('eeee', 'fff', NULL, NULL);
下面我们对各个字段进行具体分析:
变长字段长度列表
基本概念:
将真实数据中所有可变长类型的字段所占用的字节长度记录下来,逆序排列形成一个列表放在行的头部。
为什么需要该列表
因为MySQL
服务器需要知道每个字段的长度,但是可变长类型字段的长度是未知的,所以需要把这些字段的长度保存下来。
例子:
test表的第一行数据中的可变长字段列表:
字段 长度 16进制
aaaa 4 0x04
bbb 3 0x03
d 1 0x01
所以对于第一行数据,其Compact行格式中存储的变长字段长度列表为:010304,在整个行格式中可以表示为:
字节数判断
上面只用一个字节就表示可变长字段长度,因为字段实在太小,但我们实际开发中可能会遇到长度很大的字段,这时候一个字节就不够用了。对于何时用一个字节、何时用两个字节,InnoDB
有自己的一套规则:
下面先声明一下规则中的W、M、L分别是什么意思:
W:当前字符集一个字符能占用的最大字节数。例如
utf8
中是3(之前说过MySQL
中utf8
是指utf8mb3
),gbk
中是2,ASCII是1等;M:当前可变长字段的类型允许的最大字符数。例如
varchar(10)
表示varchar
这种类型只允许最多10个字符;L:可变长字段占用的的实际字节数。例如"
aaaa
"字段占用的实际字节数是4,因为其可以用ASCII字符表示,而ASCII一个字符用一个字节表示;
具体规则如下:
- 当
W * M <= 255
时,用一个字节表示; - 当
W * M > 255
,且L > 127
时,用两个字节表示; - 当
W * M > 255
,且L < 127
时,用一个字节表示。
针对上述规则,你可能有如下疑问:
- 为什么
W * M
是和255比较,而L
和127比较呢?
前者是因为一个字节有8位,2^8就是256,即表示0-255;
后者是因为在
InnoDB
中,可变长字段的每个字节中第一个二进制位是标志位,不作为具体数据存储位置;标志位作用:区分当前阅读的字节是一个完整的字段长度还是半个字段长度(如果字段是两个字节表示的,当前阅读字节就是半个字段长度)。
当标志位等于0时,表示是一个完整的字段长度,等于1表示是半个字段长度。
注意:
变长字段长度列表内只存储满足以下全部条件的字段:
- 字段是可变长类型
- 字段非空
NULL值列表
基本概念:
是用来存储记录中的所有NULL值字段的一个列表,用一个二进制位表示是否为NULL,值等于1表示该字段为NULL,同样也是逆序排列。
为什么需要该列表
该列表可以用二进制位来表示字段是否为NULL,这样可以减少空间的消耗。如果没有该列表,记录中的NULL值就只能存储在真实数据的存储区,而NULL值有时也是需要空间的,这样会导致空间浪费。
当NULL值所在列的类型为定长类型时(如"CHAR()“类型),其就会占用定长的内存;当NULL值所在列类型为变长类型时(如"VARCHAR
”),则不占用任何内存。
例子:
test表中的第二行记录如下:
字段 值
c1 eeee
c2 fff
c3 NULL
c4 NULL
其中c3、c4
字段为NULL,所以这两位二进制位应该为1,其余为0,表示为:
因为字段c2
被声明为了NOT NULL,所以其不会被添加在NULL值列表内。
注意:
- 可以没有NULL值列表:如果记录中全部字段均不为NULL,或者全部字段均设置了NOT NULL约束,那么NULL值列表就不存在;
- NULL值列表要求必须用整字节来表示NULL值:如果记录中NULL值数小于1个字节,就用0补齐高位,如果大于1个字节,小于2个字节,就用两个字节表示,第二个字节用0补齐高位。
记录头信息
基本信息
记录行性质的相关信息,如行是否有效、是否已被删除、行在堆中的位置、下一条记录的相对位置等等。
组成结构
由固定的五个字节组成,不同位有不同位的功能,其中高位的前两个二进制位是预留位。
具体位与功能图如下:
名称 | 大小(bit) | 功能 |
---|---|---|
预留位1、2 | 各1bit | 未使用 |
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
还会有选择的添加几个新的列。新列如下表:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
DB_ROW_ID | 否 | 6 字节 | 行 ID,唯一标识一条记录 |
DB_TRX_ID | 是 | 6 字节 | 事务 ID |
DB_ROLL_PTR | 是 | 7 字节 | 回滚指针 |
InnoDB
表选取主键的策略:
- 若有自定义主键,优先选取自定义的主键;
- 若没有自定义主键,则选取一个带有
Unique
约束的列作为主键; - 若自定义主键、
Unique
约束都没有,则会添加新的列DB_ROW_ID
作为主键。
记住,MySQL
会为每一行记录添加列DB_TRX_ID
和列DB_ROLL_PTR
。而只有当表中自定义主键和Unique
约束都没有时,MySQL
才会添加列DB_ROW_ID
作为主键。
CHAR(M)
的特殊格式
CHAR(M)的含义:
最多能包含 M 个字符,但少于 M 个字符时,其所占空间与包含 M 个字符相同。
优点:
更容易将空间对齐,不容易产生空间碎片。
特殊格式:对于不同的字符集,在"可变长字段列表"中有不同的行为。
- 当所属字符集是单字节字符集(最大字符长度为1个字节)时,
CHAR(M)
不会被添加到"可变长字段列表"中; - 当所属字符集是多字节字符集时,
CHAR(M)
会被添加到"可变长字段列表"中。
Redundant
行格式
这是一种很古老的一种行格式,被使用在MySQL5.0
时代,所以现在看来是有点过时的。有意思的是,这种行格式的名字Redundant
本身就有"淘汰、累赘"的意思(手动doge)。
这种行格式的示意图如下:
下面具体来说说各个字段的含义:
字段长度偏移列表
相较于Compact
行格式,这种行格式没有了"可变长字段长度列表",而添加了个"字段长度偏移列表"。
基本概念
记录所有列在"记录真实数据区"的偏移字节量,同样,也是逆序的。
需要记住两个点:
- 记录所有列的偏移
- 逆序排列偏移量
记录头信息
大小为 6 个字节。相较于Compact
行格式,这种行格式多了n_field
和1byte_offs_flag
属性,少了record_type
属性。下面对多了的属性进行解释:
名称 | 大小(bit) | 描述 |
---|---|---|
n_field | 10 | 该行记录中包含的列数 |
1byte_offs_flag | 1 | 表示每个列的偏移量是一个字节表示还是两个字节表示的。1表示1个字节,0表示2个字节 |
CHAR(M)
的存储方式
Redundant
行格式下的CHAR(M)
不用分情况讨论,只有一个存储方式:字符集的最大表示字节数 × M。如GBK
的最大表示字节数位2,假设M=10,那么GBK
下的CHAR(10)
就占20个字节。
Dynamic
和Compressed
行格式
这两个行格式与Compact行格式很像,只不过在处理行溢出时的行为不太一样,这两个行格式不会在记录页存储数据,而是把溢出的数据全都放在溢出页,在记录页只存放溢出页的地址。
行溢出
当一个行中存储了很大的数据时,可能会发生行溢出。
在Compact
和Redundant
行格式下发生行溢出之后,当前页中存储格式为:部分数据+溢出页地址。
在Dynamic
和Compressed
行格式下发生行溢出,当前页中只存储溢出页地址。