一、前言
最近在读《MySQL 是怎样运行的》、《MySQL技术内幕 InnoDB存储引擎 》,后续会随机将书中部分内容记录下来作为学习笔记,部分内容经过个人删改,因此可能存在错误,如想详细了解相关内容强烈推荐阅读相关书籍。
系列文章内容目录:
- 【MySQL00】【 杂七杂八】
- 【MySQL01】【 Explain 命令详解】
- 【MySQL02】【 InnoDB 记录存储结构】
- 【MySQL03】【 Buffer Pool】
- 【MySQL04】【 redo 日志】
- 【MySQL05】【 undo 日志】
- 【MySQL06】【MVCC】
- 【MySQL07】【锁】
- 【MySQL08】【死锁】
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。即磁盘预读原理。
InnoDB 中页的大小一般为 16KB, 因此利用磁盘预读原理在一般情况下 InnoDB 利用磁盘预读原理,一次读写操作的都是 16KB,即每次读写都是操作一个 InnoDB 页的大小。
二、InnoDB 行格式
在创建表或修改表的时候可以执行记录所使用的行格式, 如下:
CREATE TABLE [tableName] [列信息] ROW_FORMAT=行格式名称;
ALTER TABLE [tableName] ROW_FORMAT=行格式名称;
InnoDB 有四种不同类型的行格式:COMPACT 、REDUNDANT、DYNAMIC、COMPRESSED,具体如下:
1. COMPACT 行格式
COMPACT 行格式示意图如下:
基于上图,大体分为如下部分:
1.1 记录的额外信息
-
变长字段长度列表:用于记录 VARCHAR、TEXT 等变长字段的实际长度。因为变长字段的长度是不固定的,所以需要一个额外的地方存储变长字段的实际长度列表,该部分记录在【记录的额外信息】的【变长字段长度列表】中,而字段实际内容在【记录的真实数据】部分。
需要注意的是:
- 【变成字段长度列表】是按照字段定义逆序存放。因为记录的next_record 指针指向【记录的额外信息】和【记录的真实数据】之间,向左则是头信息,向右则是数据信息。
- 变长字段长度列表只会存储值为非 NULL 的列的字段长度,不存储值为 NULL 的列的内容长度。
- 如果表中所有列都不是变长数据类型或者所有列的值都是 NULL 的话,就不需要有变长字段长度列表。
-
NULL 值列表:一条记录中的某些列可能存储 NULL 值,如果把这些 NULL值都放到记录的真实数据中存储起来会占用空间,所以COMPACT 行格式把一条记录中值为 NULL 的列统一管理起来,存储到NULL 值列表。
需要注意的是:
- NULL 值列表不是必须的,如果没有 NULL 列则就不存在该头信息。
-
记录头信息:由固定的5字节组成,用于描述记录一些属性。具体如下:
名称 大小(比特) 描述 预留位1 1 未使用 预留位2 1 未使用 deleted_flag 1 标记该记录是否被删除 min_rec_flag 1 B+Tree 中每层非叶子节点中的最小的目录项记录都会添加该标记 (索引页为1,数据页为0) n_owned 4 一个页面中的记录会被分成若干个组(Page Directory),其中每组的最后一条记录中的 头信息中的 n_owned 属性表示该组中共有几条记录。 head_no 13 表示当前记录在页面堆中的相对位置,每申请一条记录的存储空间时,head_no 加1 record_type 3 表示当前记录的类型,0 表示普通记录,1 表示 B+Tree 非叶子节点的目录项记录,2 表示 Infimum 记录,3 表示 Supremum 记录 next_record 16 表示下一条记录的相对位置。
1.2 记录的真实数据
除了真实数据列外,MySQL会为每行记录增加几个隐藏列:
列名 | 是否必需 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6字节 | 行ID,如果记录不满足主键生成策略,则使用该列,如果该行记录已经有了主键,则该属性不需要创建。 |
trx_id | 是 | 6字节 | 事务id |
roll_pointer | 是 | 7字节 | 回滚指针 |
1.3 综上
-
deleted_flag :标记该记录是否被删除,需要注意的是:
- 当记录被删除后,并不会从磁盘中移除,因为移除它们之后,还需要再磁盘上重新排列其他记录。因此为了避免这种性能消耗,所以只打一个删除标记。所有被删除的记录会形成一个垃圾链表,记录在这个链表中占用的空间成为可重用空间。
-
next_record : 表示下一条记录的相对位置,需要注意的是:
- 这里指向的位置是下一条行记录的 【记录的额外信息】和【记录的真实信息】之间的位置,如果该值为负数,则说明下一条记录在当前记录前面,否则在后面。因为记录是以链表形式指向,所以不需要严格的物理顺序性。
- 这里的下一条记录指的并不是插入顺序中的下一条记录,而是按照主键值从小到大的顺序排列的下一条记录,如果一个记录被删除,则 deleted_flag 会被标记为 1,当下次插入的时候,可能会复用该空间(如果主键合适的话)
- 之所以指向 【记录的额外信息】和【记录的真实信息】之间的位置因为在这个位置,向左读取就是记录头信息,向右读取就是真实数据,并且 变长字段长度列表、NULL 值列表都是逆序存放到,因此使得记录中位置靠前的字段和他对应的字段长度信息在内存中距离更近,这可能会提高高速缓存命中率。如下图:
-
一条记录的结构如下(其中隐藏列row_id、trx_id、roll_pointer没有画出,并不是不存在):
2. REDUNDANT 行格式
REDUNDANT 是 MySQL 5.0 之前就在使用的一种古老的行格式。格式如下:
2.1 字段长度偏移列表
REDUNDANT 行格式的 字段长度列表 会将所有字段的长度信息都按照逆序存储到字段长度偏移列表。
2.2 记录头信息
REDUNDANT 记录头信息占用 6 字节,如下:
需要注意的是:
- REDUNDANT 行格式对于 NULL 值的处理:将列对应的偏移量值的第一个比特位作为是否为 NULL 的依据,该比特位可以称之为 NULL 比特位,如果该位为1 则该列的值为 NULL,否则就不是 NULL。
3. DYNAMIC 行格式和 COMPPESED 行格式
DYNAMIC 行格式和 COMPPESED 行格式与 COMPACT 行格式基本类似。区别在与处理溢出页时不会在记录的真实数据处存储该溢出列真实数据的前768字节,而是把该列的所有真实数据都存储到溢出页中,只在记录的真实数据处存储20字节大小指向溢出页的地址。
COMPPESED 与 DYNAMIC 不同的是:COMPPESED 行格式会采用压缩算法对页面进行压缩。
三、InnoDB 数据页结构
InnoDB 数据页默认 16KB,可以划分为多个部分,如下:
各个字段如下:
名称 | 中文名 | 占用空间大小(字节) | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38 | 页的一些通用信息 |
Page Header | 页面头部 | 56 | 数据页专有的一些信息 |
Infimum + Supremum | 页面中的最小记录和最大记录 | 26 | 两个虚拟的记录,MySQL硬性规定,Infimum 记录页面中最小的记录,Supremum记录页面中最大的记录。即任何用户写的记录都比 Supremum 记录小,比 Infimum 记录大 (类似于链表中的头节点和尾节点) |
User Records | 用户记录 | 不确定 | 用户存储的记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页目录 | 不确定 | 页中某些记录的相对位置 |
File Trailer | 文件尾部 | 8 | 校验页是否完整 |
1. File Header (文件头部)
File Header 通用与各种类型的页,也就是说各种类型的页都以 File Header 作为第一个组成部分,他描述了一些通用于各种页的信息。该部分固定占用38字节。包括 每页的页号(InnoDB 通过页号确定唯一页)、前后页的地址(形成双向链表)、页的类型(undo 日志页、数据页、change buffer 页等),具体如下:
状态名称 | 占用空间大小(字节) | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | MySQL 4.014 版本代表本页所属表空间Id,之后表示页的校验和 |
FIL_PAGE_OFFSET | 4 | 页号, 每个页都有一个单独的页号 |
FIL_PAGE_PREV | 4 | 上一个的页号,InnoDB 允许页之前不连续,前后的页通过链表连接 |
FIL_PAGE_NEXT | 4 | 下一个的页号,InnoDB 允许页之前不连续,前后的页通过链表连接 |
FIL_PAGE_LSN | 8 | 页面被最后修改时对应的 LSN值 |
FIL_PAGE_TYPE | 2 | 该页的类型(undo 日志页、数据页、change buffer 页等) |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 仅在系统表空间的第一个页中定义,代表文件至少被刷新到了对应的LSN 值 |
FIL+PAGE_ARCH_LOG_NO_OR_SPACE_IF | 4 | 页属于哪个表空间 |
2. Page Header (页面头部)
File Header 通用与各种类型的页, 而Page Header 针对的是数据页记录的各种状态。Page Header 是在数据页中的特殊部分,存储在数据页中的记录的状态信息,比如:数据页中已经存储了多少条记录, Free Space 在页面中的地址偏移量,页目录中存储了多少槽等。具体如下:
状态名称 | 占用空间(字节) | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在页目录中的槽数量 |
PAGE_HEAP_TOP | 2 | 还未私用的空间最小地址,也就是说从该地址之后就是 Free Space |
PAGE_N_HEAP | 2 | 第一位表示本记录是否为紧凑型记录,剩15位表示本页的堆中记录的数量 |
PAGE_FREE | 2 | 各个已删除的记录通过 next_record 组成的一个单向链表,这个链表中的记录所占用的空间可以被重新利用;PAGE_FREE 表示该链表头节点对应记录在页面中的偏移量 |
PAGE_GARBAGE | 2 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2 | 最后插入记录的位置 |
PAGE_DIRECTION | 2 | 记录插入的方向 ,如果新插入的记录主键值比上一条记录大,则认为这个记录的插入方向是右侧,否则是左边 |
PAGE_N_DIRECTION | 2 | 一个方向连续插入的记录数量,如果多条记录连续插入方向相同,则通过该参数记录下来条数,如果最后一条记录的插入方向发送改变,则这个状态的值会清零后重新统计 |
PAGE_N_RECS | 2 | 该页中用户记录的数量(不包括 Infimum 、Supremum以及被删除的记录) |
PAGE_MAX_TRX_ID | 2 | 修改当前页的最大事务ID,该值仅在二级索引页面中定义 |
PAGE_LEVEL | 2 | 当前页在B+Tree 中所处的层级 |
PAGE_INDEX_ID | 8 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10 | B+Tree 叶子节点段的头部信息,仅在 B+Tree 的根页面中定义 |
PAGE_BTR_SEG_TOP | 10 | B+Tree 非叶子节点段的头部信息,仅在 B+Tree 的根页面中定义 |
3. Infimum + Supremum
两个虚拟的记录,MySQL硬性规定,Infimum 记录页面中最小的记录,Supremum记录页面中最大的记录。即任何用户写的记录都比 Supremum 记录小,比 Infimum 记录大 (类似于链表中的头节点和尾节点),如下图:
4. User Records + Free Space
一开始生成页的时候,并没有 User Records 部分,每当插入一条记录的时候会从 Free Space 部分(尚未使用的部分)申请一个记录大小的空间,并将空间划到 User Records 部分。当 Free Space 部分的空间全部被 User Records 部分替代后,就意味着页使用完了,需要申请新的页了。
在 User Records 中,一条条记录亲密无间的排列结构称为堆。为了方便管理堆,将一条记录在堆中的相对位置成为head_no, head_no 单调递增,每条插入的记录都有其对应的 head_no。
5. Page Directory
记录在页中是按照主键值由小到大的顺序串联成一个单向链表。如果想要根据主键值查找页中的某条记录,如果从 Infimum 到 Supremum 中查找,则效率太低,因此InnoDB 采用 Page Directory 的方式对记录做了目录(利用二分查找的思想)。制作过程如下:
- 将所有正常的记录划分为几个组(包括 Infimum + Supremum 记录,但不包括已经移除到垃圾链表的记录),分组的规定如下:
- Infimum 记录分组只能有一条记录。即只有 Infimum 一条记录,所以 Infimum 记录的 n_owned 值为 1, 因为该分组有且只有一条记录
- Supremum 记录所在的分组只能有1-8条之间。
- 其他分组中的记录只能是4-8条之间。
- 每个组的最后一条记录中的 头信息中的 n_owned 属性表示该组中共有几条记录。
- 将每组中最后一条记录在页面中的地址偏移量(就是该记录的真实数据与页面中第0个字节之间的距离)单独提取出来,按顺序存储到靠近页尾部的地方。这个地方就是 Page Directory(页目录)。页目录中的这些地址偏移量成为 槽,每个槽占用 2字节。页目录就是由多个槽组成的。
如下图:
6. File Trailer
占用 8 字节,用于验证页写入是否完整。
四、InnoDB 表空间
所有的数据都被逻辑地存放在一个空间中,称为表空间(tablespace), 表空间由 段(segment)、区(extent)、页(page)组成,页在一些文档中页称为块(block),如下图
基础概念如下:
-
表空间(tablespace) :表空间可以看是做 InnoDB 结构的最高层,所有数据都存放在表空间中。InnoDB 支持多种类型的表空间,而表空间是一个抽象概念,对于系统表空间来说对应着文件系统中的一个或多实际文件;对于每个独立表空间来说,对应文件系统中一个名为 “表名.ibd” 的实际文件。
在默认情况下 InnoDB 有一个系统表空间 ibdata1,即所有的数据都存放在这个表空间中。而如果设置了 innodb_file_per_table 参数,则可以将每个基于 InnoDB 的表产生一个独立的表空间。但是需要注意这里的独立表空间文件进存储该表的数据、索引和插入缓冲 BITMAP 等,其余信息(如回滚信息、插入缓冲索引页、系统事务信息、二次写缓冲等)还是存放在默认表空间中。这也就是说,即使启用了 innodb_file_per_table 参数,ibdata1的大小仍会增加,并且在事务回滚后大小因为事务而增大的空间并不会被回收(即 ibdata1 不会减小),但是会被判断这些空间是否还需要,如果不需要则标记为可用空间,供下次使用,那么下次提交事务时 ibdata1 大小则可能不会再增长。 -
段(segment) :表空间由段组成,常见的段有数据段、索引段、回滚段等。段并不对应表空间的某一个连续的物理区域,而是一个逻辑上的概念:B+Tree 的叶子节点和非叶子节点进行区分,否则叶子节点和非叶子节点统统在一个区则查询效果会大打折扣。因此叶子节点和非叶子节点都有自己独有的区。而存放叶子节点的区的集合就是一个 段(Leaf node sgement),存放非叶子节点点的区的集合也算是一个段(Non-Leaf node segment)。也就是说一个索引会生成一个 叶子结点段 和一个 非叶子节点段。
-
区(extent) :对于 16KB 的页来说,连续 64 个页就是一个区(extent), 即一个区默认占 1MB 空间大小。无论是系统表空间还是独立表空间,都可以看做是由若干个连续的区组成的,每 256 个区被划分成一组。 其中第一个组最开始的3个页时固定的,即 Extend0 这个区最开始3个页面类型是固定的,其余各组最开始的两个页类型是固定的。
总结:表空间被划分为了许多连续的区,每个区默认由 64 个页组成,每256个区划分为一组,每个组最开始的几个页面类型是固定的。
在 InnoDB 1.0.x 版本中引入了压缩页,即每个页的大小可以通过参数控制,1.2.x版本可以设置默认页的大小,但是无论页的大小怎么变化,区的大小总是1M,无非就是每个区对应的页的数量不同。在使用了 innodb_file_per_table 参数后创建的表默认大小事 96KB,而区的大小是1M,为了解决这种情况,在每个段开始时先用32个连续页大小的碎片页(fragment page) 来存放数据,在使用完这些页后才是64个连续页的申请,目的是对于一些小表或者undo这类的段可以在开始时申请较小的空间,节省磁盘容量开销。(那就会出现,磁盘占用跳跃的情况,即一个区用完后即使插入1KB数据也会增加1M磁盘开销的情况)
每256个区被划分成一组,每个组的最开始几个页面类型是固定的。
注意:
-
为什么要引入区 (extend) 的概念?
因为要尽量使得页面链表中相邻的页的物理位置页相邻。这样扫描叶子节点中大量的记录时才可以使用顺序 IO。如果不使用区,则页与页之间物理位置可能离得非常远,而对于传统机械硬盘来说,如果双向链表中相邻的两个页物理位置不连续,则需要重新定位磁头位置。因此引入了 区(extend) 的概念。一个区就是在物理位置上连续的64个页(区里的页面的页号都是连续的)。在表数据非常多的时候,为某个索引分配空间的时候就不再按照页为单位进行分配,而是以区位单位分配。甚至在表中数据非常非常多的时候,可以一次性分配多个连续的区。虽然可能会造成一些空间浪费(区的空间未填满)但是可以消除很多随机 IO。 -
碎片区的概念
默认情况下,一个索引生成两个段(存放叶子节点的段和存放非叶子节点的段),而段是以区位单位,因此一个段至少有一页即1M,即一个索引至少占用2M空间?实际并非如此,因为存在碎片(fragment)区的概念。也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,碎片区中的页可以用于不同的目的,如有些页可以属于段A,有些页可以属于段B,有些页不属于任何段,因此碎片区直属于表空间,并不属于任何一个段。因此为某个段分配存储空间的策略如下:
- 在刚开始向表中插入数据时,段是从某个碎片区以单个页面为单位来分配存储空间的。
- 当某个段已经占用了32 个碎片区页面之后就会以完整的区位单位来分配存储空间(原先占用的碎片区页面并不会被复制到新申请的完整的区中。)
因此更精确的来说,段是某些零散的页面以及一些完整的区的集合。
-
区的分类
状态名 含义 FREE 空闲区 FREE_FRAG 有剩余空闲页面的碎片区 FULL_FRAG 没有剩余空闲页面的碎片区 FSEG 附属于某个段的区
-
-
页(page): 页也被成为 块。页是InnoDB 磁盘管理的最小单位。默认16Kb,可通过参数调整大小,但是调整后不可再修改,除非通过mysqldump 导入导出重新建表。
1. 独立表空间
1.1 XDES Entry (Extent Descriptor Entry)
每个区都对应着一个 XDES Entry 结构,这个结记录了对应的区的一些属性。结构如下:
从上图看出,每个 XDES Entry 结构有40字节,大致划分如下:
- Segment ID (8字节) : 每个段都有自己的唯一编号,用Id表示。如果当前区被分配给了某个段,这里保存的就是段该区所在段的ID,如果该区没被分配给某个段,则该字段值没有意义。
- List Node(12字节) :这个部分可以将若干个 XDES Entry 结构串成一个链表,结构如上图。如果我们想定位表空间内的某一个位置,只需要指定页号以及该位置在指定页号中的页内偏移量即可。
- State(4字节) :这个字段表名区的状态。可选值分别是 FREE(空闲区)、FREE_FRAG(有剩余空闲页面的碎片区)、FULL_FRAG(没有剩余空闲页面的碎片区)、FSEG(附属于某个段的区)
- Page State Bitmap(16字节) :该部分占用16字节,即128位。一个区默认64个页,即每两位对应该区中的一个页,第一位表示是否空闲,第二位还未用到。
1.1.1 XDES Entry 链表
当向段中插入数据时,申请新页面的过程:当段中数据较少时,首先会查看表空间中是否有状态为 FREE_FRAG 的区(空闲页面的碎片区),如果找到了则从该区中取出一个零散页把数据插进去;否则到表空间申请一个状态为 FREE的区 (空闲的区),把该区的状态变为 FREE_FRAG ,然后从该新申请的区中取出一个零散页把数据插入。之后在不同的段使用零散页的时候都从该区中取,直到该区中没有空闲页面;然后该区的状态就变成了 FULL_FRAG(没有剩余空闲页面的碎片区)。
在上述过程中,我们需要知道表空间的区的状态(FREE、FREE_FRAG、FULL_FRAG以及FSEG),而当表空间不断增大时,区的数量也会随之增加,如果通过遍历区的方式来获取区的状态则效率会非常低下(因为当表空间增长到 GB级别,区的数量也会增加到几千个),所以该部分是通过 XDES Entry 的 List Node 部分来实现 : 通过 List Node 把状态为 FREE的区对应的 XDES Entry结构连接成一个链表,这个链表成为 FREE 链表,同理还存在 FREE_FRAG 链表、FULL_FRAG 链表。这样当想查找一个状态为 FREE_FRAG 状态的区时,直接就把 FREE_FRAG 链表的头节点拿出来,从这个节点对应的区中取一些零散页来插入数据。当这个节点对应的区中没有空闲页时,就修改其 State 字段值,然后将其从 FREE_FRAG 链表移动到 FULL_FRAG 链表中。同理,如果 FREE_FRAG 链表中一个节点都没有,那么直接从 FREE 链表中取出一个节点移动到 FREE_FRAG 链表,并修改该节点的 State 字段值为 FREE_FRAG ,然后再从这个节点对应的区中获取零散页即可。当段中的数据已经占满32 个零散的页后,就直接申请完整的区来插入数据了。
综上:每个段中的区对应的 XDES Entry 结构建立了 3 个链表:
- FREE 链表:同一个段中,所有页面都是空闲页面的区对应的 XDES Entry 结构会被加入到这个链表中。需要注意的是此处的 FREE 链表并不是直属表空间的 FREE 链表,而是附属于某个段的链表。
- NOT_FULL 链表:同一个段中,仍有空闲页面的区对应的 XDES Entry 结构会被加入到这个链表中。
- FULL 链表:同一个段中,已经没有空闲页面的区对应的 XDES Entry 结构会被加入到这个链表中。
即:每一个索引都对应两个段,每个段都会维护上述三个链表。
1.1.2 链表基节点
InnoDB通过链表基节点(List Base Node)来在表空间中快速定位链表吗,其结构如下:
- List Length : 表名该链表中一共有多少节点
- First Node Page Number 和 First Node Offset 表示该链表的头结点在表空间中的位置。
- Last Node Page Number 和 Last Node Offset 表示该链表在尾节点在表空间中的位置。
具体结构如下图:
综上:表空间是由若干区组成,每个区都对应一个 XDES Entry 结构。直属于表空间的区对应的 XDES Entry 结构可以分为 FREE、FREE_FRAG 和 FULL_FRAG 三个链表。每个段可以拥有若干个区,每个段中的区对应的 XDES Entry 结构可以构成 FREE、NOT_FULL 和 FULL 这三个链表。每个链表对应一个 List Base Node 结构,这个结构中记录了链表的头尾节点的位置以及该链表中包含的节点数。
1.2 段的结构
1.2.1 基础结构
段是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。和每个区都有 XDES Entry 类似,每个段中都定义了一个 INODE Entry 结构来记录这个段中的属性。
- Segment ID :当前 INODE Entry 结构对应的 段 ID。
- NOT_FULL_N_USED :在 NOT_FULL 链表中已经使用了多少页面
- 3 个 List Base Node :分别为段的 FREE链表、NOT_FULL 链表、FULL链表定义了 List Base Node,这样当想查找某个链表的头结点和尾节点时,可以直接到这个部分找到对应链表的 List Base Node。
- Magic Number :用来标记这个 INODE Entry 是否已经被初始化(即把哥哥字段的值都填进去了)。如果Magic Number = 97937874 则表名已经初始化。
- Fragment Array Entry :由于段是一些零散页面和一些完整的区的集合。每个 Fragment Array Entry结构都对应着一个零散的页面,这个结构一共 4字节,表示一个零散页面的页号。
整体结构如下图:
1.2.2 Segment Header
一个索引会产生两个段,分别是叶子节点段和非叶子节点段,而每个段都会对应一个 INODE Entry ,因此还需要保存一个段跟 INODE Entry 的对应关系。
在 上面提到数据页的 Page Header 部分时,存在两个结构 PAGE_BTR_SEG_LEAF 和 PAGE_BTR_SEG_TOP ,这两个部分都占用10个字节,对应的都是一个 Segment Header 结构如下:
各个部分的解释如下
名称 | 解释 |
---|---|
Space ID of the INODE Entry | INODE Entry 结构所在的表空间 ID |
Page Number of the INODE Entry | INODE Entry 结构所在的页面页号 |
Byte Offset of the INODE Entry | INODE Entry 结构在该页面中的偏移量 |
综上:PAGE_BTR_SEG_LEAF 和 PAGE_BTR_SEG_TOP 分别记录叶子节点和非叶子节点段对应的INODE Entry 结构的地址是哪个表空间中哪个页面的哪个偏移量。这样索引和对应的段的关系就建立起来了。不过因为一个索引只对应两个段,所以只需要在索引根页面中记录这两个结构即可。
1.3 总结
INODE Entry 与 XDES Entry 的关系图如下:
每个段都对应一个 INODE Entry 结构,每个 INODE Entry 结构中存在三个 List Base Node 链表:分别为段的 FREE链表、NOT_FULL 链表、FULL链表,指向在当前段空间内状态为 FREE、NOT_FULL 和 FULL 状态的区的信息。当段进行空间分配的时候,可以通过 List Base Node 链表可以快速找到 FULL 或者 NOT_FULL 的区信息并根据区的 XDES Entry 结构确定区中的页的空闲状态来分配页
2. 系统表空间
与独立表空间相比系统表空间开头有许多记录整个系统属性的页面。如下:
可以看到系统表空间和独立表空间的前三个页面(页号为 0,1,2,类型为 FSR_HDR、IBUF_BITMAP、INODE)的类型是一致的,但是页号3-7的页面是系统表空间独有的,如下:
页号 | 页面类型 | 英文描述 | 描述 |
---|---|---|---|
3 | SYS | Insert Buffer Header | 存储 Change Buffer 头部信息 |
4 | INDEX | Insert Buffer Root | 存储 Change Buffer 的根页面 |
5 | TRX_SYS | Transaction System | 事务系统的相关信息 |
6 | SYS | First Rollback Segment | 第一个回滚段的信息 |
7 | SYS | Data Directory Header | 数据字典头部信息 |
五、补充内容
1. CHAR(M) 列的存储格式
-
在 COMPACT 行格式下,变长字段长度列表值用来存放一条记录中的变长字段值占用的字节长度,而 CHAR 类型不属于变长字段类型,理应不应该保存在边长字段长度列表中。
但上述情况仅仅建立在表使用 ascii 字符集的情况下,因为 ascii 是一个定长字符集,如果采用变长编码的字符集(也就是表示一个字符需要的字节不确定,比如 gbk 表示一个字符要 1-2 个字节 utf-8 表示一个字符需要 1-3个字节),这种情况下即使是 列是 CHAR 类型,该列的值占用的字节仍让会被已存储到变长字段长度列表中。即:对于 CHAR(M) 类型的列来说,当列采用的是定长编码字符集时,该列占用的字节数不会被加到变长字段长度列表;而如果采用变长编码的字符集时,该列占用的字节数就会被加到变长字段长度列表。
另外,COMPACT 行格式还规定,采用变长编码字符集的 CHAR(M) 类型的列要求至少占用 M 个字节,而 VARCHAR(M) 并没有这个要求。如对于一个采用 utf8 字符集的 CHAR(10) 的列来说,该列的存储占用的字节长度的范围就是 10-30字节(因为 utf8 字符集表示一个字符需要1-3个字节),即使存储的是一个空字符串也会占用10字节。
-
在 REDUNDANT 行格式下,只要使用了 CHAR(M) 类型,该列的真实数据占用的存储空间大小就是该字符集表示一个字符最多需要的字节数的类型,如对于一个采用 utf8 字符集的 CHAR(10) 的列来说,其真实数据占用的存储空间大小始终为30字节。
2. 溢出列
一个页的大小一般是16KB,即16384 个字节,如果某一列数据存储65532个字节数据,那么一个页就存放不下,此时就会出现溢出列。对于溢出列的处理,不同的行格式处理方案不同。
- COMPACT 和 REDUNDANT 格式中会在记录列数据的地方存储数据中的部分数据:768 个字节 + 20 个指向真正数据页的指针。如果真正数据页存储不下,数据页会指向下一个数据页,形成链表结构。
- DYNAMIC 和 COMPRESSED 在处理溢出列的方案有些不同,主要是他们不会存储 768 个字节 + 20个指针,而是直接存储指向真实数据的20字节的指针。
如下图:
注意:
- 产生溢出页的临界点:MySQL 规定一个页中至少存放两行记录(不是两列),否则就需要通过溢出列来完成该规定。
- 存放正常记录的页和溢出页时两种不同的类型,对于溢出页来说并没有规定一个页中至少存放两条记录。
六、参考内容
书籍:《MySQL是怎样运行的——从根儿上理解MySQL》、《MySQL技术内幕 InnoDB存储引擎 》
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正