InnoDB 页简介

InnoDB 是一个将表中的数据存储到磁盘上的存储引擎。
真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中处理。
如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。
而读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB 存储引擎不会一条一条的把记录从磁盘上读出来。
InnoDB 存储引擎采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位InnoDB 中页的大小一般为 16 KB
也就是在一般情况下,一次最少从磁盘中读取 16KB 的内容到内存中,一次最少把内存中的 16KB 内容刷新到磁盘中。

InnoDB 行格式

我们是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式
设计 InnoDB 存储引擎的人到现在为止设计了 4 种不同类型的行格式,分别是:Compact、Redundant、Dynamic 和 Compressed 行格式。


指定行格式的语法
我们可以在创建或修改表的语句中指定行格式:

  1. create table 表名 (列的信息) row_format=行格式名称
  2. alter table 表名 row_format=行格式名称

Compact 行格式

Compact 行格式是在 MySQL5.0 中引入的,是为了高效的存储数据,简单的说就是:为了让一个页存放的行数据越多,这样性能就越高。
Compact 行格式的结构如下图所示:
图片.png

  • 记录的额外信息:是服务器为了描述这条记录而不得不额外添加的一些信息

下面详细说明这两部分的组成。

变长字段长度列表

MySQL 支持一些变长的数据类型,比如 varchar(M)、varbinary(M)、各种 text 类型,各种 blob 类型,
我们把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,
所以在存储真实数据时需要把这些数据占用的字节数也存起来,所以这些变长字段占用的存储空间分为两部分:真正的数据内容、数据占用的字节数。
在 Compact 行格式中,各变长字段的真实数据内容占用的字节数按照列的顺序逆序存放在变长字段长度列表中。


如果变长字段的真实数据内容占用的字节数比较小,则用 1 个字节就可以表示字节数,
但是如果变长字段的真实数据内容占用的字节数比较多,可能就需要用 2 个字节来表示字节数。

一个字节可以表示的最大值是 127,最小值是 -128。


真实数据内容占用的字节数具体用 1 个字节还是 2 个字节来表示,InnoDB 有它的一套规则,首先声明一下 W、M 和 L 的意思:

  • 假设某个字符集中表示一个字符最多需要使用的字节数为 W,也就是使用 show charset; 语句的结果中的 Maxlen 列,比方说 utf8 字符集是 3,gbk 字符集是 2,ASCII 字符集是 1
  • 对于变长类型 varchar(M),这种类型表示能存储最多 M 个字符(注意是字符不是字节),所以这个类型能表示的字符串最多占用的字节数就是 M × W
  • 假设它实际存储的字符串占用的字节数是 L

确定使用 1 个字节还是 2 个字节表示真实数据内容占用的字节数的规则如下

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

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

  • 如果 M × W > 255,则再分为两种情况:
    • 如果 L <= 127,则用 1 个字节来表示真正字符串占用的字节数。
    • 如果 L > 127,则用 2 个字节来表示真正字符串占用的字节数。

InnoDB 存储引擎在读:记录的变长字段长度列表时先查看表结构,如果某个变长字段允许存储的最大字节数 > 255 时,该怎么区分它正在读的某个字节是一个单独的字段长度还是半个字段长度呢?
设计 InnoDB 存储引擎的人使用该字节的第一个二进制位作为标志位:如果该字节的第一个位为 0,那该字节就是一个单独的字段长度,

使用一个字节表示 <= 127 的二进制的第一个位都为 0

如果该字节的第一个位为 1,那该字节就是半个字段长度。


对于一些占用字节数非常多的字段,比方说某个字段占用字节数 > 16KB,那么如果该记录在单个页面中无法存储完时,InnoDB 存储引擎会把一部分数据存放到所谓的溢出页中,在变长字段长度列表处只存储留在本页面中的长度,所以使用两个字节也可以存放下来。


总结一下就是说:如果该可变字段允许存储的最大字节数 (M × W) 超过 255 字节并且真实存储的字节数 (L) 超过 127 字节,使用 2 个字节存储真实数据内容占用的字节数,否则使用 1 个字节存储。


对于 char(M) 类型的列来说
当列采用定长字符集时如 ASCII,该列真实数据内容占用的字节数不会被加到变长字段长度列表,
当列采用变长字符集时如 utf8,该列真实数据内容占用的字节数也会被加到变长字段长度列表。


还有一点需要注意:
变长字符集如 utf8 的 char(M) 类型的列要求至少占用 M 个字节,而 varchar(M) 没有这个要求。
比方说:使用 utf8 字符集的 char(10) 的列来说,该列存储的数据字节长度的范围是10~30个字节。
即使我们向该列中存储一个空字符串也会占用 10 个字节
这么设计是怕将来更新该列的值的字节长度 > 原有值的字节长度而 < 10 个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间称为所谓的碎片。


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

变长字段长度列表中只存储值为 非null 的列内容占用的长度,值为 null 的列的长度是不储存的。

null 值列表

道表中的某些列可能存储 null 值,如果把这些 null 值都放到记录的真实数据中存储会很占地方,
所以 Compact 行格式把值为 null 的列统一管理起来,存储到 null 值列表中,它的处理过程如下:

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

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

  • 如果表中没有允许存储 null 的列,则 null 值列表 也不存在了,否则将每个允许存储 null 的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下:
    • 二进制位的值为 1 时,代表该列的值为 null
    • 二进制位的值为 0 时,代表该列的值不为 null
  • MySQL 规定 null 值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补 0。

即 null 值列表必须用,值如:8、16、24 等表示。
如果一个表中有超过 8 个允许为 null 的列,那这个记录的 null 值列表部分就需要用大于 1 个字节来表
示。

记录头信息

记录在页中的存储 里面对记录头信息的讲解更详细
记录头信息是由固定的 5 个字节组成。
5个字节也就是 40 个二进制位,不同的位代表不同的意思,这些二进制位代表的详细信息如下表:

名称 大小(bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
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 表示下一条记录的相对位置

图片.png

记录的真实数据

记录的真实数据除我们自己定义的列的数据以外,
MySQL会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下:

列名 是否必须 占用空间 描述
row_id 6字节 行 ID,唯一标识一条记录
transaction_id 6字节 事务 ID
roll_pointer 7字节 回滚指针

小贴士:
实际上这几个列的真正名称是: DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR, 为了美观才写成了表中所示。


InnoDB 存储引擎对主键的生成策略

  • 优先使用用户自定义主键作为主键
  • 如果用户没有定义主键,则选取一个 Unique 键作为主键
  • 如果表中连 Unique 键都没有定义的话,则 InnoDB 存储引擎会为表默认添加一个名为 row_id 的隐藏列作为主键

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


真实记录的存储格式如下:
图片.png

行溢出数据

MySQL 对一条记录占用的最大存储空间是有限制的,
一个表中除了 blob 或者 text 类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。
MySQL 中磁盘和内存交互的基本单位是 页,
也就是说 MySQL 是以页为基本单位来管理存储空间的,
我们的记录都会被分配到某个页中存储。
而一个页的大小一般是 16KB,也就是 16384 字节,这就可能造成一个页存放不了一条记录的尴尬情况。
在 Compact 和 Reduntant 行格式中,对于占用存储空间非常大的列,
在记录的真实数据处只会存储该列的前 768 个字节的数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用 20 个字节存储指向这些页的地址,从而可以找到剩余数据所在的页,
这 20 个字节中还包括这些分散在其他页面中的数据的占用的字节数。
这个过程也叫做行溢出,存储超出 768 个字节的那些页面也被称为溢出页。
如图所示:
图片.png


行溢出的临界点
发生行溢出的临界点是什么呢?
也就是说在列存储多少字节的数据时就会发生行溢出?
MySQL 中规定一个页中至少存放两行记录
看一下这个规定造成的影响。
假设我们分析的表只有一个列,我们往这个表中插入两条记录,每条记录最少插入多少字节的数据才会发生行溢出的呢?
这得分析一下页中的空间都是如何利用的。

  • 每个页除了存储我们的记录以外,还需要存储一些额外的信息,

这些额外的信息加起来需要 136 个字节的空间,其他的空间都可以被用来存储记录。

  • 每个记录需要的额外信息是 27 字节。这 27 个字节包括下边这些部分:
    • 2 个字节用于存储真实数据的长度
    • 1 个字节用于存储 null 值列表
    • 5 个字节用于存储记录头信息
    • 6 个字节 row_id 列
    • 6 个字节的 transaction_id 列
    • 7 个字节的 roll_pointer 列

假设一个列中存储的数据字节数为 n,则发生行溢出现象时需要满足这个式子:136 + 2×(27 + n) > 16384
求解这个式子得出的解是:n > 8098。
也就是说:如果一个列中存储的数据 <= 8098 个字节,那就不会发生行溢出,否则就会发生行溢出。
这个 8098 个字节的结论只是针对只有一个列的表来说的,如果表中有多个列,那上边的式子和结论都需要改。
所以重点就是只需要知道:如果一个行中存储了很大的数据时,可能发生行溢出现象

Redundant 行格式

Redundant 是 MySQL5.0 之前的行记录格式。
图片.png
《MySQL 是怎样运行的》专栏中有详细介绍,按需学习。

Dynamic & Compressed 行格式

MySQL5.7版本的默认行格式就是 Dynamic。
这两个行格式和 Compact 行格式相似,只不过在处理行溢出数据时有点儿分歧,
Dynamic 和 Compressed 行格式不会在记录的真实数据处存储字段的前 768 个字节,
而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址,就像这样:
图片.png
Compressed 与 Dynamic 不同的一点是:
Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。