该篇是学习笔记,笔记来源于《MySQL是怎样运行的:从根儿上理解MySQL》
InnoDB 是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还 需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我 们想从表中获取某些记录时,InnoDB 存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死, InnoDB 采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小 一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB 内容刷新到磁盘中
InnoDB行格式
1.COMPACT行格式
大家从图中可以看出来,一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分,下边我 们详细看一下这两部分的组成。
1.1记录的额外信息
这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是变长字段 长度列表、NULL值列表和记录头信息,我们分别看一下
变长字段长度列
我们知道MySQL 支持一些变长的数据类型,比如VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型,各种 BLOB 类 型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我 们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把MySQL服务器搞懵,所以 这些变长字段占用的存储空间分为两部分:
- 真正的数据内容
- 占用的字节数
在Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长 字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放。
当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表
列名 | 存储内容 | 内容长度(十进制表示) | 内容长度(十六进制表示) |
c1 | 'aaaa' | 4 | 0x04 |
c2 | 'bbb' | 3 | 0x03 |
c3 | 'd' | 1 | 0x01 |
第1条记录的存储格式:
NULL 值列表
我们知道表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地方,所 以Compact 行格式把这些值为 NULL 的列统一管理起来,存储到 NULL 值列表中
如果表中没有允许存储 NULL 的列,则 NULL 值列表不存在,,否则将每个允许存储NULL 的列对应一个 二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下
- 二进制位的值为1时,代表该列的值为NULL。
- 二进制位的值为0时,代表该列的值不为NULL。
如果此时有一个表 test ,有列 c1,c2,c3 其中三个列都允许存储null值则这3个列和二进制位的对应关系就是这样:
MySQL 规定 NULL值列表 必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节 的高位补0。
表test 只有3个值允许为 NULL 的列,对应3个二进制位,不足一个字节,所以在字节的高位补0,效果就是这样
所以,如果test表中有两条记录 1,2,3 和1,null,null
他们所对应的NULL值列表 应该如下图:
记录头信息
名称 大小(单位: 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 表示下一条记录的相对位置
混个脸熟即可~
1.2 记录的真实数据
对于test表来说,记录的真实数据除了 c1 、 c2 、 c3 这几个我们自己定义的列的数据 以外,MySQL 会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下
列名 | 是否必须 | 占用空间 | 描述 |
DB_ROW_ID | 否 | 6 字节 | 行ID,唯一标识一条记录 |
DB_TRX_ID | 是 | 6 字节 | 事务ID |
DB_ROLL_PTR | 是 | 7 字节 | 回滚指 |
InnoDB 表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则 选取一个Unique 键作为主键,如果表中连 Unique 键都没有定义的话,则 InnoDB 会为表默认添加一个名为 row_id 的隐藏列作为主键。所以我们从上表中可以看出:InnoDB存储引擎会为每条记录都添加 transaction_id 和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。 这些隐藏列的值不用我们操心,InnoDB 存储引擎会自己帮我们生成的。
因为test表 并没有定义主键,所以 MySQL 服务器会为每条记录增加上述的3个列。
Redundant行格式
的Redundant 行格式是 MySQL5.0 之前用的一种行格式,比较老了。
字段长度偏移列表
Compact 行格式的开头是 变长字段长度列表,而Redundant 行格式的开头是 字段长度偏移列表 ,与 变长字段长度列表有两处不同
- 没有了变长两个字,意味着Redundant 行格式会把该条记录中所有列(包括隐藏列)的长度信息都按 照逆序存储到字段长度偏移列表。
- 多了个偏移两个字,这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数 值的差值来计算各个列值的长度
行溢出数据
对于VARCHAR(M) 类型的列最多可以占用 65535 个字节。其中的 M 代表该类型最多存储的字符数量,如 果我们使用ascii 字符集的话,一个字符就代表一个字节,我们看看VARCHAR(65535) 是否可用
MySQL 对一条记录占用的最大存储空间是有限制的,除了BLOB或者TEXT类型的列之 外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。所以MySQL服 务器建议我们把存储类型改为TEXT或者BLOB 的类型。这个65535 个字节除了列本身的数据之外,还包括一些 其他的数据(storage overhead ),比如说我们为了存储一个 VARCHAR(M) 类型的列,其实需要占用3部分存储 空间:
- 真实数据
- 真实数据占用字节的长度
- NULL 值标识,如果该列有NOT NULL属性则可以没有这部分存储空间
如果该VARCHAR 类型的列没有 NOT NULL 属性,那最多只能存储 65532 个字节的数据,因为真实数据的长度可能 占用2个字节,NULL 值标识需要占用1个字节:
记录中的数据太多产生的溢出
如上图 ,REPEAT('a', 65532) 是一个函数调用,它表示生成一个把字符 'a' 重复 65532 次的字符串。前边说 过,MySQL 中磁盘和内存交互的基本单位是页,也就是说MySQL是以页为基本单位来管理存储空间的,我们 的记录都会被分配到某个页中存储。而一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M) 类 型的列就最多可以存储65532 个字节,这样就可能造成一个页存放不了一条记录的尴尬情况。
在Compact 和 Reduntant 行格式中,对于占用存储空间非常大的列,在 记录的真实数据 处只会存储该列的一部 分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用20个字节存储指向这些页的地址 (当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。如图:
不只是 VARCHAR(M) 类型的列,其他的 TEXT、BLOB 类型的列在存储数据非常多的时候 也会发生行溢出
Dynamic和Compressed行格式
mysql 5.7以后默认的行格式为Dynamic 这俩行格式和 Compact 行格式挺像,只不过在处理 行溢出 数据时有点儿分歧,它们不会在记 录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数 据处存储其他页面的地址:
Compressed 行格式和 Dynamic 不同的一点是, Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。