InnoDB行记录的结构
前言
本文是《从根上理解MySQL》第四章的读书笔记,主要介绍InnoDB的一条行记录的结构。
一、InnoDB页简介
MySQL 服务器上负责对表中数据的读取和写入工作的部分是存储引擎,MySQL默认的存储引擎是InnoDB。真实数据在不同存储引擎中存放的格式一般是不同的,而InnoDB是以InnoDB页的形式存放数据的。
InnoDB是将表中的数据存储到磁盘的存储引擎,所以关机重启后数据还在。数据处理的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存,若是修改和写入的情况,还需把内存中的内容刷新到磁盘。读写磁盘速度比直接内存读写的速度相差非常大,为了解决直接读写磁盘慢的问题,InnoDB采取:将数据分为若干页,以页作为磁盘和内存交互的基本单位,页一般大小为16KB。
二、InnoDB行格式
记录是像表中插入数据的单位,记录在磁盘上存放形式称为行格式或者记录格式。行格式有四种类型:COMPACT、REDUNDANT、DYNAMIC、COMPRESSED。
2.1 指定行格式的语法
在创建或修改表的语句中指定行格式:
CREATE TABLE 表名 (
列的信息
) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
2.2 COMPACT行格式
可以看出,一条完整的记录分为:记录的额外信息与记录的真实数据。
2.2.1 记录的额外信息
这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是变长字段长度列表、NULL值列表和记录头信息。
a. 变长字段长度列表
变长字段中存储多少字节的数据是不固定的,如:VARCHAR(M) 、VARBINARY(M) 、各种TEXT 类型,各种BLOB 类型,把拥有这些数据类型的列称为变长字段。
变长字段占用的存储空间包括:真正的数据内容与占用的字节数。
在Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放,注意是逆序。如:
——例子:
创建一个表,指定字符集为ascii,指定行格式为COMPACT:
CREATE TABLE record_format_demo (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10), -- 注意c3不是变长字段
c4 VARCHAR(10)
) CHARSET=ascii ROW_FORMAT=COMPACT;
记住,我们采用的是ascii字符集,后面讲的都是针对这个字符集!!!!!ascii字符集中表示一个字符最多需要使用1个字节数。
插入两条数据:
INSERT INTO record_format_demo(c1, c2, c3, c4) VALUES('aaaa', 'bbb', 'cc', 'd'),
('eeee', 'fff', NULL, NULL);
第一条记录各变长字段内容的长度:
变长字段长度列表的字节串用十六进制表示的效果就是:04 03 01 。这些长度值需要按照列的逆序存放,第一条记录存储里:
——确定使用1个字节还是2个字节表示真正字符串占用的字节数的规则:
由于第一行记录中c1 、c2 、c4 列中的字符串都比较短,也就是说内容占用的字节数比较小,用1个字节就可以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。确定字节数,需先明白这几个概念:
- W:某个字符集中表示一个字符最多需要使用的字节数。如:utf8 字符集为3 、 gbk 字符集为2 、ascii 字符集为1。
- M:对于变长类型VARCHAR(M),M表示这种类型最多能存储的字符个数。
- L:实际存储的字符串占用的字节数。
确定使用1个字节还是2个字节表示真正字符串占用的字节数的规则就是这样:
如果可变字段允许存储的最大字节数( M x N )超过255字节并且真实存储的字节数( L)超过127字节,则使用2个字节,否则使用1个字节。
需要注意,变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的。如果表中所有的列都不是变长的数据类型的话,变长字段长度列表就没有了。
b. NULL值列表
表中的某些列可能存储NULL 值,Compact 行格式把这些值为NULL 的列统一管理起来,存储到NULL 值列表中。当然,如果表中没有允许存储 NULL 的列,则NULL值列表也将不存在。
对一条记录里NULL值的处理过程:
- 统计表中允许存储 NULL 的列有哪些。主键列、被 NOT NULL 修饰的列 都是不可以存储NULL 值的。
- 将每个允许存储 NULL 的列对应一个二进制位,二进制位按照列的顺序逆序排列。二进制位表示的意义:二进制位的值为1 时,代表该列的值为 NULL ;为0 时,代表该列的值不为 NULL 。
- MySQL 规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0 。
上面创建的表record_format_demo中,只有3个值允许为NULL 的列(c1、c3、c4),对应3个二进制位,不足一个字节,所以在字节的高位补0。对于第一条记录,('aaaa', 'bbb', 'cc', 'd')
,都不为NULL,所以对应二进制为1:
所以第一条记录的NULL值列表用十六进制表示就是: 0x00 。而第二条记录,('eeee', 'fff', NULL, NULL)
,c3、c4为NULL,对应二进制位为1:
所以第二条记录的NULL值列表用十六进制表示就是: 0x06 。
当然,如果一个表中有9个允许为NULL ,那这个记录的NULL 值列表部分就需要2个字节来表示了。
所以这两条记录在填充了NULL值列表后的示意图就是这样:
c. 记录头信息
记录头信息由固定的5 个字节组成。5 个字节也就是40 个二进制位,不同的位代表不同的意思,如图:
各二进制位的含义:
名称 | 大小(位) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_flag | 1 | 标记该记录是否被删除 |
min_rec_flag | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned | 4 | 表示当前记录拥有的记录数 |
heap_no | 13 | 表示当前记录在记录堆的位置信息 |
record_type | 3 | 表示当前记录的类型, 0 表示普通记录, 1 表示B+树非叶子节点记录, 2 表示最小记录, 3表示最大记录 |
next_record | 16 | 表示下一条记录的相对位置 |
这部分会在数据页里有讲解。
2.2.2 记录的真实数据
记录的真实数据除了c1 、c2 、c3 、c4 这种自己定义的列的数据以外, MySQL 会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6字节 | 行ID,唯一标识一条记录 |
transaction_id | 是 | 6字节 | 事务ID |
roll_pointer | 是 | 7字节 | 回滚指针 |
实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR,为了美观写成了row_id、transaction_id和roll_pointer。
注意!!!!提一下InnoDB 表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique 键作为主键,如果表中连Unique 键都没有定义的话,则InnoDB 会为表默认添加一个名为row_id 的隐藏列作为主键。
从上表中可以看出:InnoDB存储引擎会为每条记录都添加 transaction_id和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。
加上记录的真实数据的两个记录:
看这个图的时候我们需要注意几点:
- 表record_format_demo 使用的是ascii 字符集,所以0x61616161 就表示字符串’aaaa’ ,0x626262 就表示字符串’bbb’ ,以此类推。
- 注意第1条记录中c3 列的值,它是CHAR(10) 类型的,它实际存储的字符串是: ‘cc’ ,而ascii 字符集中的字节表示是’0x6363’ ,虽然表示这个字符串只占用了2个字节,但整个c3 列仍然占用了10个字节的空间(char是定长的),除真实数据以外的8个字节的统统都用空格字符填充,空格字符在ascii 字符集的表示就是0x20 。
- 注意第2条记录中c3 和c4 列的值都为NULL ,它们被存储在了前边的NULL值列表处,在记录的真实数据处就不再冗余存储,从而节省存储空间。
2.2.3 CHAR(M)列的存储格式
因为前文里采用的是ASCII字符集,它是一个定长的字符集(用一个字节表示一个字符)。而对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。因为采用变长字符集,意味着CHAR类型的字段长度也不确定了。
——例子:
比如,修改一下record_format_demo 表的字符集为utf8:
ALTER TABLE record_format_demo MODIFY COLUMN c3 CHAR(10) CHARACTER SET utf8;
可以看到变化:
有一点还需要注意,变长字符集的CHAR(M) 类型的列要求至少占用 M 个字节,向该列中存储一个空字符串也会占用M 个字节。而VARCHAR(M) 却没有这个要求,VARCHAR(M)的M表示最多占用M 个字节。
2.3 REDUNDANT行格式
Redundant 行格式是MySQL5.0 之前用的一种行格式,了解就好了。其结构:
将表的行格式修改为REDUNDANT:
ALTER TABLE record_format_demo ROW_FORMAT=Redundant;
记录变为:
2.3.1 与CPMPACT对比
a. 字段长度偏移列表
注意Compact 行格式的开头是变长字段长度列表,而Redundant 行格式的开头是字段长度偏移列表。
没有了变长两个字,意味着Redundant 行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。偏移:用两个相邻数值的差值来计算各个列值的长度。
比如:第一条记录的字段长度偏移列表就是:25 24 1A 17 13 0C 06。逆序:06 0C 13 17 1A 24 25。按照两个相邻数值的差值来计算各个列值的长度:6,6(0x0C - 0x06),7(0x13 - 0x0C)… 以此类推。
b. 记录头信息
Redundant 行格式的记录头信息占用6 字节, 48 个二进制位,这些二进制位代表的意思如下:
名称 | 大小(位) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_flag | 1 | 标记该记录是否被删除 |
min_rec_flag | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned | 4 | 表示当前记录拥有的记录数 |
heap_no | 13 | 表示当前记录在记录堆的位置信息 |
n_field | 10 | 表示记录中列的数量 |
1byte_offs_flag | 1 | 标记字段长度偏移列表中每个列对应的偏移量是使用1字节还是2字节表示 |
next_record | 16 | 使用1字节还是2字节表示的 |
与Compact 行格式的记录头信息对比来看,有两处不同:
-
Redundant 行格式多了n_field 和1byte_offs_flag 这两个属性。
-
Redundant 行格式没有record_type 这个属性。
c. 1 byte_offs_flag 的值如何选择
字段长度偏移列表实质上是存储每个列中的值占用的空间在记录的真实数据处结束的位置。前边说过每个列对应的偏移量可以占用1个字节或者2个字节来存储,那到底什么时候用1个字节,什么时候用2个字节呢?其实是根据条Redundant 行格式记录的真实数据占用的总大小来判断的:
- 记录的真实数据占用的字节数不大于127(十六进制0x7F ,二进制01111111 )时,每个列对应的偏移量占用1个字节。
- 字节数大于127,但不大于32767时,每个列对应的偏移量占用2个字节。
- 记录的真实数据大于32767的情况,此时的记录已经存放到了溢出页中,在本页中只保留前768 个字节和20个字节的溢出页面地址(当然这20个字节中还记录了一些别的信息)。
d. NULL 值的处理
Redundant 行格式并没有NULL值列表,取而代之的是,在字段长度偏移列表中的各个列对应的偏移量处做了一些特殊处理 —— 将列对应的偏移量值的第一个比特位作为是否为NULL 的依据,该比特位也可以被称之为NULL比特位。
e. CHAR(M)列的存储格式
Compact 行格式在CHAR(M) 类型的列中存储数据的时候,分变长字符集和定长字符集的情况。而在Redundant 行格式中十分干脆,不管该列使用的字符集是啥,只要是使用CHAR(M) 类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和M 的乘积。
比方说使用utf8 字符集的CHAR(10) 类型的列占用的真实数据空间始终为30 个字节,使用gbk 字符集的CHAR(10) 类型的列占用的真实数据空间始终为20 个字节。由此可以看出来,使用Redundant 行格式的CHAR(M) 类型的列是不会产生碎片的。
2.4 行溢出数据
MySQL 中磁盘和内存交互的基本单位是页,MySQL 是以页为基本单位来管理存储空间的。所有的记录都会被分配到某个页中存储,而一个页的大小一般是16KB ,也就是16384 字节。而一个VARCHAR(M) 类型的列就最多可以存储65532 个字节,这样就可能造成一个页存放不了一条记录的尴尬情况。
在Compact 和Reduntant 行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。
如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出,存储超出768 字节的那些页面也被称为溢出页,这一列叫做溢出列。溢出列的真实数据,简图如下:
不只是 VARCHAR(M) 类型的列,其他的 TEXT、BLOB 类型的列在存储数据非常多的时候也会发生行溢出。
2.5 Dynamic和Compressed行格式
Dynamic 和Compressed 行格式,和Compact 行格式挺像,只不过在处理行溢出数据时有点儿分歧。它们不会在记录的真实数据处存储字段真实数据的前768 个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
Compressed 行格式和Dynamic 不同的一点是, Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。