MySQL高级(五)
五、InnoDB数据存储结构
1. 数据页内部结构
大小:默认16KB
1.1 第1部分
1.1.1 File Header(38字节)
描述各种页的通用信息。(比如页的编号、其上一页、下一页是谁等)
大小:38字节
-
FIL_PAGE_OFFSET(4字节)
:InnoDB通过页号可以唯一定位一个页。 -
FIL_PAGE_TYPE(2字节)
:页的类型。 -
FIL_PAGE_PREV(4字节)和FIL_PAGE_NEXT(4字节)
:保证这些页之间不是物理上的连续,而是逻辑上的连续,即底层数据页是通过双向链表连接的。 -
FIL_PAGE_SPACE_OR_CHKSUM(4字节)
: 检验和,如果校验和不一样,则两个长字节串肯定不同。 -
FIL_PAGE_LSN(8字节)
:页面被最后修改时对应的日志序列
位置(英文名是:Log Sequence Number)。
1.1.2 File Trailer(8字节)
- 前4个字节代表页的校验和:和File Header中的校验和相对应的。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):校验页的完整性的,如果首部和尾部的LSN值校验不成功,说明同步过程出现了问题。
1.2 第2部分
1.2.1 Free Space(空闲空间)
每当插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分 ,当Free Space部分的空间全部被User Records部分替代掉之后,意味着这个页使用完了,如果还有新的记录插入的话,需要去 申请新的页。
1.2.2 User Records(用户记录)
User Records
中的这些记录按照 指定的行格式 存储,形成 单链表 。
1.2.3 Infimum和Supremum(最小最大记录)
由5字节大小的记录头信息和8字节大小的一个固定的部分组成
1.3 第3部分
1.3.1 Page Directory(页目录)
在页中,记录是以 单向链表 的形式进行存储,单向链表的特点就是插入、删除非常方便,但是 检索效率不高 ,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块, 专门给记录做一个目录 ,通过 二分查找法 的方式进行检索,提升效率。
页目录:
- 将所有的记录 分成几个组 ,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。
- 第 1 组,也就是最小记录所在的分组只有 1 个记录; 最后一组,就是最大记录所在的分组,会有 1-8 条记录;其余的组记录数量在 4-8 条之间。除了第 1 组(最小记录所在组)以外,其余组的记录数会 尽量平分 。
- 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为
n_owned
字段。 - 页目录用来存储每组最后一条记录的地址偏移量 ,这些地址偏移量会按照 先后顺序存储 起来,每组的地址偏移量也被称之为 槽(slot) ,每个槽相当于指针指向了不同组的最后一个记录。
举例:
现在的page_demo表中正常的记录共有6条,InnoDB会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录。如下图:
注意:
InnoDB规定:对于最小记录所在的分组只能有1条记录,最大记录所在的分组拥有的记录条数只能在1~8条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
在一个数据页中查找指定主键值的记录的过程分为两步:
通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
通过记录的next_record属性遍历该槽所在的组中的各个记录。
1.3.2 Page Header(页头)
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节,专门存储各种状态信息。
2. InnoDB行格式(或记录行格式)
我们平时的数据以行为单位来向表中插入数据,这些记录在磁盘上的存放方式也被称为
行格式
或者记录格式
。InnoDB存储引擎设计了4种不同类型的行格式
,分别是Compact
、Redundant
、Dynamic
和Compressed
行格式。
SELECT @@innodb_default_row_format;
2.1 指定行格式的语法
在创建或修改表的语句中指定行格式:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称 ALTER TABLE 表名 ROW_FORMAT=行格式名称
2.2 COMPACT行格式
在MySQL 5.1版本中,默认设置为Compact行格式。一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。
2.2.1 变长字段长度列表
- MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型,这些数据类型修饰列称为 变长字段 ,变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。 在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。
- 注意:这里面存储的变长长度和字段 顺序是反过来 的。比如两个varchar字段在表结构的顺序是a(10),b(15)。那么在变长字段长度列表中存储的长度顺序就是15,10,是反过来的。
2.2.2 NULL值列表
-
Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。
-
之所以要存储NULL是因为数据都是需要对齐的,如果 没有标注出来NULL值 的位置,就有可能在查询数据的时候 出现混乱 。如果使 用一个特定的符号 放到相应的数据位表示空置的话,虽然能达到效果,但是这样很 浪费空间 ,所以直接就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据,格式如下:
-
二进制位的值为1时,代表该列的值为NULL。
-
二进制位的值为0时,代表该列的值不为NULL。
-
2.2.3 记录头信息
各个属性如下:
delete_mask
:值为0:代表记录并没有被删除;值为1:代表记录被删除掉了。- 这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后其他的记录在磁盘上需要 重新排列,导致性能消耗 。所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的 垃圾链表 ,在这个链表中的记录占用的空间称之为 可重用空间 ,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
heap_no
:从编号2
开始,MySQL会自动给每个页里加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为 伪记录 或者 虚拟记录 。这两个伪记录一个代表 最小记录 ,一个代表 最大记录 。最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前。next_record
:表示从当前记录的真实数据到下一条记录的真实数据的 地址偏移量。
2.2.4 记录的真实数据
DB_ROW_ID
(6字节):非必需,唯一标识行IDDB_TRX_ID
(6字节):必需,事务IDDB_ROLL_PTR
(7字节):必需,回滚指针
一个表没有手动定义主键,则会选取一个Unique键作为主键,如果连Unique键都没有定义的话,则会为表默认添加一个名为row_id的隐藏列作为主键。所以row_id是在没有自定义主键以及Unique键的情况下才会存在的。
2.3 Dynamic和Compressed行格式
2.3.1 行溢出
InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。
- 一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为 行溢出 。
- 在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中进行 分页存储 ,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。
2.3.2 Dynamic和Compressed行格式
在MySQL 8.0中,默认行格式就是Dynamic。Dynamic、Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有分歧:
-
Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。
-
Compact和Redundant两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)。
-
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。
2.4 Redundant行格式
Redundant行格式的首部是一个字段长度偏移列表,同样是按照列的顺序 逆序放置 的。
2.4.1 字段长度偏移列表
注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:
-
少了“变长”两个字:Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。
-
多了“偏移”两个字:这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
2.4.2 记录头信息
不同于Compact行格式,Redundant行格式中的记录头信息固定占用6个字节(48位),每位的含义见下表。
-
与Compact行格式的记录头信息对比来看,有两处不同:
-
Redundant行格式多了n_field和1byte_offs_flag这两个属性。
-
Redundant行格式没有record_type这个属性。
-
其中,n_fields:代表一行中列的数量,占用10位,这也很好地解释了为什么MySQL一个行支持最多的列为1023。另一个值为1byte_offs_flags,该值定义了偏移列表占用1个字节还是2个字节。当它的值为1时,表明使用1个字节存储。当它的值为0时,表明使用2个字节存储。
-
-
1byte_offs_flag的值是怎么选择的
我们前边说过每个列对应的偏移量可以占用1个字节或者2个字节来存储,那到底什么时候用1个字节,什么时候用2个字节呢?其实是根据该条Redundant行格式记录的真实数据占用的总大小来判断的
- 当记录的真实数据占用的字节数值不大于127(十六进制0x7F,二进制01111111)时,每个列对应的偏移量占用1个字节。
- 当记录的真实数据占用的字节数大于127,但不大于32767(十六进制0x7FFF,二进制0111111111111111)时,每个列对应的偏移量占用2个字节。
- 当记录的真实数据大于32767的情况,此时的记录已经存放到了溢出页中,在本页中只保留前768个字节和20个字节的溢出页面地址。因为字段长度偏移列表处只需要记录每个列在本页面中的偏移就好了,所以每个列使用2个字节来存储偏移量就够了。
- Redundant行格式中NULL值的处理
因为Redundant行格式并没有NULL值列表,所以Redundant行格式在字段长度偏移列表中的各个列对应的偏移量处做了一些特殊处理 —— 将列对应的偏移量值的第一个比特位作为是否为NULL的依据,该比特位也可以被称之为NULL比特位。也就是说在解析一条记录的某个列时,首先 看一下该列对应的偏移量的NULL比特位是不是为1。 如果为1,那么该列的值就是NULL,否则不是NULL。
这也就解释了上边介绍为什么只要记录的真实数据大于127(十六进制0x7F,二进制01111111)时,就采用2个字节来表示一个列对应的偏移量,主要是第一个比特位是所谓的NULL比特位,用来标记该列的值是否为NULL。
- 如果存储NULL值的字段是定长类型的,比方说
CHAR(M)
数据类型的,则NULL值也将占用记录的真实数据部分,并把该字段对应的数据使用0x00字节填充。 - 如果该存储NULL值的字段是变长数据类型的,则不在记录的真实数据处占用任何存储空间。