Administrator
发布于 2022-11-15 / 52 阅读
0
0

InnoDB记录结构

前言:本文内容是对《MySQL是怎样运行的》的摘抄

简介:

当我们想从表中获取某些记录时, InnoDB 存储引擎并不是一条一条的把记录从磁盘上读出来。
InnoDB 采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

InnoDB行格式

行格式:
4种不同类型的 行格式 ,分别是 Compact 、 Redundant 、
Dynamic 和 Compressed 行格式

COMPACT行格式

image-1668478592873

指定行格式
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称

 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 支持一些变长的数据类型,比如 VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型,各种 BLOB 类
型,我们也可以把拥有这些数据类型的列称为 变长字段 ,变长字段中存储多少字节的数据是不固定的,所以我
们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把 MySQL 服务器搞懵,所以
这些变长字段占用的存储空间分为两部分:

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

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

eg:record_format_demo
c1 、 c2 、 c4 列都是 VARCHAR(10) 类型的,也就是变长的数据类型,所以这三个列的值的长度都需要保存在记录开头处,因为record_format_demo 表中的各个列都使用的是 ascii 字符集,所以每个字符只需要1个字节来进行编码

image-1668478951898

因为这些长度值需要按照列的逆序存放,所以最后变长字段长度列表的字节串用十六进制表示的效果就是 01 03 04

image-1668479076557

填入进表中的示意图:

image-1668567180042

第一行记录中 c1 、 c2 、 c4 列中的字符串都比较短,也就是说内容占用的字节数比较小,用1个字节就可
以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。具体用1个还是2个字节来
表示真实数据占用的字节数, InnoDB 有它的一套规则,我们首先声明一下 W 、 M 和 L 的意思:

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

最后的结论是:如果该可变字段允许存储的最大字节数( M×W )超过255字节并且真实存储的字节数( L )超过127字节,则使用2个字节,否则使用1个字节。
想知道具体分析可以看书

如果存储的列中有null值呢?就像插入的第二条记录。

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

image-1668567445841

NULL值列表

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

实现

1、首先统计表中允许存储 NULL 的列有哪些
2、如果表中没有允许存储 NULL 的列,则 NULL值列表 也不存在了,否则将每个允许存储 NULL 的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位的值为 1 时,代表该列的值为 NULL 。二进制位的值为 0 时,代表该列的值不为 NULL 。
如record_format_demo中有三列允许为空值,那么逆序存储后如图:

image-1668567704682

3、MySQL 规定 NULL值列表 必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节
的高位补 0 。
因为三列只占用三个二进制位,不足一个字节,所以高位补0。

image-1668567824955

对于record_format_demo中的另一列:

image-1668568021255

其用十六进制表示分别是:0x00 ;0x06

image-1668568073815

隐藏列

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

image-1668568329127

真正名称分别是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR

InnoDB 表对主键的生成策略:

优先使用用户自定义主键作为主键,如果用户没有定义主键,则
选取一个 Unique 键作为主键,如果表中连 Unique 键都没有定义的话,则 InnoDB 会为表默认添加一个名为
row_id 的隐藏列作为主键。所以我们从上表中可以看出:InnoDB存储引擎会为每条记录都添加 transaction_id
和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。
这些隐藏列的值不用我们操心, InnoDB 存储引擎会自己帮我们生成的。

表 record_format_demo 并没有定义主键,所以其示意图:

image-1668568469784

  1. 表 record_format_demo 使用的是 ascii 字符集,所以 0x61616161 就表示字符串 ‘aaaa’ , 0x626262 就表示字符串 ‘bbb’ ,以此类推。
  2. 注意第1条记录中 c3 列的值,它是 CHAR(10) 类型的,它实际存储的字符串是: ‘cc’ ,而 ascii 字符集中
    的字节表示是 ‘0x6363’ ,虽然表示这个字符串只占用了2个字节,但整个 c3 列仍然占用了10个字节的空
    间,除真实数据以外的8个字节的统统都用空格字符填充,空格字符在 ascii 字符集的表示就是 0x20 。
  3. 注意第2条记录中 c3 和 c4 列的值都为 NULL ,它们被存储在了前边的 NULL值列表 处,在记录的真实数据处就不再冗余存储,从而节省存储空间。

CHAR(M)列的存储格式

在Compact 行格式下只会把变长类型的列的长度逆序存到 变长字段长度列表中,
但是这只是因为我们的 record_format_demo 表采用的是 ascii 字符集,这个字符集是一个定长字符集,也就是
说表示一个字符采用固定的一个字节,如果采用变长的字符集(也就是表示一个字符需要的字节数不确定,比如
gbk 表示一个字符要12个字节、 utf8 表示一个字符要13个字节等)的话, c3 列的长度也会被存储到 变长字段长度列表 中。

ALTER TABLE record_format_demo MODIFY COLUMN c3 CHAR(10) CHARACTER SET utf8;

image-1668568703809

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

ps:
变长字符集的 CHAR(M) 类型的列要求至少占用 M 个字节,而 VARCHAR(M) 却没有这个要求。比方说对于使用 utf8 字符集的 CHAR(10) 的列来说,该列存储的数据字节长度的范围是10~30个字节。即使我们向该列中存储一个空字符串也会占用 10 个字节,这是怕将来更新该列的值的字节长度大于原有值的字节长度而小于10个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。

Redundant行格式

一些区别:

字段长度偏移列表
注意 Compact 行格式的开头是 变长字段长度列表 ,而 Redundant 行格式的开头是 字段长度偏移列表 ,与
变长字段长度列表 有两处不同:
1、没有了变长两个字,意味着 Redundant 行格式会把该条记录中所有列(包括 隐藏列 )的长度信息都按
照逆序存储到 字段长度偏移列表 。
2、多了个偏移两个字,这意味着计算列值长度的方式不像 Compact 行格式那么直观,它是采用两个相邻数
值的差值来计算各个列值的长度。

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

其他略:想了解多些还是看书吧,黏贴那么多做啥?

行溢出数据

MySQL 对一条记录占用的最大存储空间是有限制的,除了 BLOB 或者 TEXT 类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。所以 MySQL 服务器建议我们把存储类型改为 TEXT 或者 BLOB 的类型。这个 65535 个字节除了列本身的数据之外,还包括一些其他的数据( storage overhead ),比如说我们为了存储一个 VARCHAR(M) 类型的列,其实需要占用3部分存储空间:

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

如果该VARCHAR 类型的列没有 NOT NULL 属性且VARCHAR(M) 类型的列使用的是 ascii 字符集,那最多只能存储 65532 个字节的数据,因为真实数据的长度可能占用2个字节, NULL 值标识需要占用1个字节,如果有 NOT NULL 属性,那最多只能存储 65533 个字节的数据,因为此时不需要NULL 值标识:

CREATE TABLE varchar_size_demo(
  c VARCHAR(65532)
  ) CHARSET=ascii ROW_FORMAT=Compact;

如果 VARCHAR(M) 类型的列使用的不是 ascii 字符集,那 M 的最大取值取决于该字符集表示一个字符最多需要的字节数。在列的值允许为 NULL 的情况下, gbk 字符集表示一个字符最多需要 2 个字节,那在该字符集下, M 的最大取值就是 32766 (也就是:65532/2),也就是说最多能存储 32766 个字符;utf8 字符集表示一个字符最多需要 3 个字节,那在该字符集下, M 的最大取值就是 21844 ,就是说最多能存储 21844 (也就是:65532/3)个字符。这都是在表中只有一个字段的情况下说的,一定要记住一个行中的所有列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。

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

MySQL 中磁盘和内存交互的基本单位是 页 ,
也就是说 MySQL 是以 页 为基本单位来管理存储空间的,我们的记录都会被分配到某个 页 中存储。而一个页的大小一般是 16KB,也就是16384字节,而一个 VARCHAR(M) 类型的列就最多可以存储65532个字节,这样就可能造成一个页存放不了一条记录的尴尬情况。

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

在本记录的真实数据处只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出 ,存储超出 768 字节的那些页面也被称为溢出页

image-1668569775481

ps:不只是 VARCHAR(M) 类型的列,其他的 TEXT、BLOB 类型的列在存储数据非常多的时候也会发生 行溢出 。我们不用关注行溢出的临界点是什么,只要知道如果我们想一个行中存储了很大的数据时,就可能会发生 行溢出 的现象。


评论