前言
-
什么是InnoDB页
MySQL服务器中负责读写数据的是存储引擎,InnoDB是一种常用的,将表数据存储在磁盘中的存储引擎。在实际操作中,MySQL将磁盘中的数据加载到内存中,若是需要处理写入或修改,则把内存中的数据刷新到磁盘。
-
什么是行格式
数据是以记录为单位在表中存储的,每一条记录在磁盘中的存储形式被称为行格式。
行格式
InnoDB有4中行格式,分别是COMPACT、REDUNDANT、DYNAMIC和COMPRESSED。
设置行格式
创建或修改表的行格式语法如下所示:
# 创建表时指定行格式 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) ) ROW_FORMAT=DYNAMIC;
# 修改表时指定行格式
ALTER TABLE record_format_demo ROW_FORMAT=COMPACT;
初始化该表数据如下:
c1 VARCHAR(10) | c2 VARCHAR(10) NOT NULL | c3 CHAR(10) | c4 VARCHAR(10) |
---|---|---|---|
aaaa | bbb | cc | d |
eeee | fff | NULL | NULL |
COMPACT
COMPACT规定一条完整的记录可分为记录的额外信息和记录的真实数据两大部分。如下图所示:
-
记录的额外信息
(1)变长字段长度列表
在COMPACT中,可变长度的字段类型,比如VARCHAR、VARBINARY、TEXT、BLOB等,占用的存储空间分为真正的数据内容和该列占用的字节数,若采用定长数据类型,但是字符编码为可变长度,也会被记录到变长字段长度列表中。在每条记录中,变长字段的真实数据占用的字节数按照列的顺序逆序存放在变成字段长度列表中,变长字段长度列表不存储值为NULL的列所占用的长度。
若上述表为ASCII字符编码格式,则第一条记录的每一列数据对应的字节数为4、3、10、1,其中第三列为定长类型,占用10个字符,也就是10个字节。这一条记录中变长字段的长度依次是4、3、1,所以变长字段长度列表中的值为:01 03 04。
由于1个字节最多表示255个数值,所以当列中的 定义最大占用字节书不大于255时,使用1个字节即可表示该列数据的字节数;当列中的定义最大字节数大于255时,分两种情况,如果实际存储的字符串占用的字节数大与127时,使用2个字节表示变长字段长度,否则使用1个字节。
定义最大占用字节数 = 字符集所需要的最大字节数 x 表结构定义的该列最大存储字符数。InnoDB在读取记录的变长字段长度列表时会先查看表结构,例如utf8mb4单个字符最大需要占用4个字节,定义一列类型VARCHAR(10),则该列的定义最大字节数为4 x 10 = 40。
若确定定义最大占用字节数大于255时,变长字段长度表示该列的字节只有7位表示长度,第一位1表示需要两个字节表示长度,第一位为0表示一个字节就可以表示长度,也就是为什么超过127就需要2个字节表示了。更长的数据使用2字节也足够,因为记录中会将超长的数据记录在溢出页。
并不是所有记录都有变长字段长度列表,如果表中所有的列都不是变长数据类型或者所有的列的值都是NULL,就不需要变长字段长度列表。
(2)NULL值列表
除去主键和使用NOT NULL修饰的列,使用二进制位按照列的顺序,逆序对照,二进制位为1表示该列值为NULL,二进制位为0,表示该列值不为NULL。上述表中,c2为NOT NULL,所以不计入NULL值列表。
如果记录的列不足一个字节,则剩余的高位补0。如果一个表中有9个值允许为NULL的列,则使用2个字节表示。
(3)记录头信息
记录头信息由固定的5字节组成,用于描述记录的一些属性。如图所示:
-
记录的真实数据
InnoDB会为每一条记录添加三个隐藏列,如下所示。在没有自定义主键,且无非NULL的UNIQUE键时才会自动生成DB_ROW_ID,另外两列都会自动生成。
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
DB_ROW_ID | 否 | 6 | 行ID |
DB_TRX_ID | 是 | 6 | 事务ID |
DB_ROLL_PTR | 是 | 7 | 回滚指针 |
上述表中的两条记录的真实记录如下图所示:
如图所示,第一条记录中,c3列的数据类型为CHAR(10),所以不论真实数据为多少,都会被填充为10个字符。如果该列采用的字符集为变长编码方式,例如utf8,则也被视为变长字段,被记录在变长字段长度列表中。且占用字节数为该字符集最少需要占用字节数 x 字符数。以utf8为例,CHAR(10)则至少需要占用10字节,该列存储的数据占用字节长度的范围是10~30字节。这么做的好处是为了防止将来在更新列时,新值在不大于10字节时,可以直接更新记录,无需从存储空间中重新分配一个新的记录空间,造成存储碎片。
若c3为变长字符编码时,变长字段长度列表发生变化:
REDUNDANT
REDUNDANT是MySQL5.0之前就存在的古老行格式,其格式如下所示:
-
字段长度偏移表
字段长度偏移表会记录所有列(包括隐藏列和值为NULL的列)的长度信息并按照逆序存储,值为两个相邻列的偏移量,通过偏移量的差值计算各个列的长度,并非直接表示长度。
各列的长度实际是这样的:
row_id=0x06-0=6
trx_id=0x0C-0x06=6
roll_pointer=0x13-0x0C=7
c1=0x17-0x13=4
c2=0x1A-0x17=3
c3=0x24-0x1A=10
c4=0x25-0x24=1。
由于REDUNDANT没有NULL值列表,看偏移量的第一个比特位,为1则表示该列数据为NULL,否则不是NULL。这样一来,1个字节只能表示数值127以内,而不是255。如果一个列的值为NULL,若是定长数据类型,则需要使用0x00填满所有的字符长度,字符集为可长度编码时,取最大值。例如一个列的数据类型为CHAR(10),采用的字符编码为utf8,则不论该列真实数据是啥,都占用30个字节;若是变长数据类型,不占用任何存储空间,偏移量不变。
-
记录头信息
REDUNDANT行格式的记录头信息占用6字节,重点关注如下属性。
-
n_field
n_field表示记录中所有列的数量,包含隐藏列和值为NULL的列。
-
1byte_offs_flag
1byte_offs_flag标记字段长度偏移列表中每个列对应的偏移量是使用1个字节表示还是使用2个字节表示的。
记录真实数据的字节数不大于127时,1byte_offs_flag=1,偏移量占用1字节;记录的真实数据占用的字节数大于127,不大于32767时,1byte_offs_flag=0,偏移量占用2字节。简单来说,只要真实数据占用的空间总数大于127,就要用2个字节表示偏移量了。更大的数据,就是溢出页的范畴了。
DYNAMIC
与COMPACT类似,区别在于溢出列的处理方式有不同。COMPACT会将前768个字节存储在记录中,把剩余数据存储在溢出列。DYNAMIC则是把所有数据都存储在溢出页中,在记录中只存储20个字节用来表示溢出页地址和数据占用字节数。
COMPRESSED
与DYNAMIC相同,不同的是COMPRESSED采用压缩算法对页面进行压缩。
REDUNDANT古老且非紧凑,COMPACT、DYNAMIC、COMPRESSED较新且紧凑,有利于节省空间。
溢出列
溢出列又称off-page列,在COMPACT和REDUNDANT行格式中,对于占用存储空间非常多的列,在记录的真实数据中只会存储该列的一部分数据,把剩余的数据分散存储在若干个其他页中,然后在记录的真实数据中用20个字节存储指向这些页的地址和所占用的字节数。
例如这里有一条记录,真实数据处只会存储该列前768个字节的数据以及一个指向其他页的地址。
溢出页如何产生的?
MySQL规定每页至少存放两行记录,每个页除了存放记录以外,也需要存储一些额外的信息,这些信息共132字节。
每个记录需要点额外信息是27字节
132 + 2✖️(27 + n) < 16384
n <8099
如果一个列超过8099字节,则会称为溢出列,但是考虑到一条记录可能有多列,只需要考虑数据非常多时,就可能成为溢出列。
索引页
索引页结构
我们知道,页是存储引擎和server层交换数据的基本单位。InnoDB有很多不同类型的页,比如存放表空间头部信息的页、存放Change Buffer信息的页、存放INODE信息的页、存放undo日志信息的页等。用户插入的记录存放在索引页中。
索引页代表的这块16KB大小的存储空间可以划分为7个部分,如下图表所示:
名称 | 中文名 | 占用空间 | 描述 |
---|---|---|---|
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
Infimum + Supremum | 页面中的最小记录和最大记录 | 26字节 | 两个虚拟的记录 |
User Records | 用户记录 | 不确定 | 用户存储的记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页目录 | 不确定 | 页中某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
记录在页中的存储
用户存储的记录会按照指定的行格式存储到User Records部分,当页完成生成时,没有User Records部分,每当插入一条记录,会从Free Space部分申请一个记录大小的空间,并将这个空间划分给User Records部分。当Free Space的空间完全被User Records代替掉后,表示这个页用完了,日吃如果还有新的记录插入,需要申请新的页。
各条记录在User Records中紧密相连,没有空隙,非常节省存储空间。
记录头信息
以COMPACT为例,记录头信息的属性如下:
名称 | 大小(位) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
deleted_flag | 1 | 标记该记录是否被删除 |
min_rec_flag | 1 | B+树中每层非叶子结点中的最小的目录项记录都会添加该标记 |
n_owned | 4 | 一个页面中的记录会被分成若干个组,每个组中最后一条记录的n_owned值代表该组中所有的记录条数,其他记录的n_owned值都为0 |
heap_no | 13 | 表示当前记录在页面堆中的相对位置 |
record_type | 3 | 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点的目录项记录,2表示Infimum记录,3表示Supremum记录 |
next_record | 16 | 表示下一条记录的相对位置 |
-
deleted_flag:表示当前记录是否被删除,值为0时表示记录没有被删除,值为1表示记录被删除。为了避免删除记录后,导致磁盘上的其他记录需要重新排列。在磁盘中,被删除的记录会组成一个垃圾链表,记录在这个链表中占用的空间称为可重用空间。后续有新记录插入到表中,可能覆盖掉被删除的记录占用的空间。
-
min_rec_flag:索引查询再说。
-
n_owned:
-
heap_no:
页中连续的记录叫做堆,一条记录在堆中的相对位置叫做heap_no。在页面前面的记录heap_no相对较小,在页面后面的记录heap_no相对较大,每申请一条新的记录存储空间时,该条记录比物理位置在它前面的那条记录的heap_no大1。另外,堆中记录的heap_no在分配以后就不会改变,即使删除堆中的某条记录,这条被删除的记录heap_no值仍然保持不变。
User Records中第一条记录的heap_no为2,因为每个页中存在两条伪记录,分别为页面的最小记录Infimum,和页面的最大记录Supremum,它们的heap_no分别为0和1。
对于一条完整的记录来说,比较记录的大小就是比较主键的大小。Infimum和Supremum虽然没有主键值,但是人为规定,任何用户记录都比Infimum记录大,任何用户记录都比Supremum记录小。
Infimum和Supremum是系统在页中默认创建的记录。其在堆中的位置相对靠前(heap_no),在InnoDB页中,它们存放在单独的Infimum+Supremum部分,并非User Records。结构如下所示:
-
record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点的目录项记录,2表示Infimum记录,3表示Supremum记录。用户插入的记录均为普通记录。
-
next_record:
表示从当前记录的真实数据到下一条记录的真实数据的距离。如果该值为正数,表示下一条记录在当前记录的后面;如果该值为负数,表示下一条记录在当前记录的前面。下一条记录指的是按主键排序的下一条记录,按照规定,Infimum记录的下一条记录就是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条记录就是Supremum记录。使用箭头代替next_record的值,如下图所示:
heap_no表示物理位置相邻的记录的排列编号,而主键按照大小排列形成的单链表为逻辑结构。
记录按照主键从小到大的顺序形成了一个单向链表,Infimum为第一个节点,Supremum为最后一个节点。如果从表中删除一条记录,但连表也会发生变化。删除第2条记录,如下所示:
记录被删除后:
-
该记录并没有从存储空间中移除,而是把该记录的deleted_flag值设置为1;
-
该记录的next_record值设置为0,因为此时该记录为页中最后一条已删除记录;
-
上一条记录的next_record指向了下一条记录,跳过该记录,继续维护单链表;
-
Supremum记录的n_owned值减1。意思是说该记录和Supremum分为同一组。
next_record指向记录头信息和真实数据之间的位置的好处是,这个位置向左读取就是记录头信息,想有读取就是真实数据,而且变长字段长度列表、NULL值列表中的信息都是逆序存放,这样提高了读取效率。
当数据页中存在多条被删除的记录时,可以使用这些记录的next_record属性将这些被删除的记录组成一个垃圾链表,也就是next_record为下一条已删除记录的偏移量。
Page Directory
Page Directory(页目录)在页中靠近页尾的地方(后面还有File Trailer部分),用来存放槽。为了避免效率低下的整页便利查找,InnoDB将页中的所有正常的记录(包括Infimum和Supremum)划分为几个组,将每个组的最后一条记录的记录头信息中n_owned属性表示这个组共有几条记录,将这条记录在页中的地址偏移量按顺序存放在页目录中,页目录中的这些地址偏移量称为槽(Slot),每个槽占用2字节。这些槽组成了页目录。
一个正常的页大小为16KB,即16384字节,而2字节可以表示的数值范围0~65535,够用了。
页目录如下图所示:
InnoDB会把页中的数据分为至少2个组,其中比较特殊的是第一个组仅有Infimum记录,Infimum记录的n_owned值为1,这表示以Infimum记录为最后一个节点的这个分组中只有1条记录,也就是Infimum记录本身,同时这个分组所对应的槽,也标记着页中的第一条记录的地址偏移量;另一个个组的最后一条记录是Supremum记录,这个组一般容纳1~8条记录,Supremum记录在页中的地址偏移量被记录在页目录中的最后一个槽中(实际是第一个,因为从大到小依次存放)。当记录越来越多时,页中会划分更多的组,这些组的记录条数只能每组4~8条之间。
页目录中的槽按照记录的大小相邻分布,槽0,也就是Infimum记录的地址偏移量存放在页目录的最末端,靠近File Trailer。
用箭头表示如下图所示:
新增一条记录时,会去找槽中比这条记录主键大的槽中最小的槽(例如0、1、2、3、4中,2、3、4都比1大,这其中2最小),将这个槽对应的记录的n_owned加1。当一个组的n_owned等于8时,再插入一条记录,会将组中的记录拆分成两个组,也就是新增一个槽,其中一个组中有4条记录,另一个组中有5条记录。
有了页目录中的槽,在主键查询时,通过二分法可以快速根据一条记录中的主键快速确定该记录所在分组对应的槽,然后找到这个槽点前面一个槽对应的最后一条记录中的next_record,找到这个槽点第一条记录,然后根据每一条记录的next_record遍历这个槽中的记录,就可以找到匹配主键的记录了。
Page Header
Page Header(页面头部)是页结构的第2部分,占用固定的56个字节,用来存储各种状态信息。Page Header中各个字节的具体用途如下所示:
状态名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2字节 | 页目录中的槽数量 |
PAGE_HEAP_TOP | 2字节 | 还未使用的空间最小地址,Free Space起始地址 |
PAGE_N_HEAP | 2字节 | 第1位表示记录是否为紧凑型记录,剩余的15位表示本页的堆中记录的数量(包括Infimum+Supremum和被标记为deleted_flag=1的记录) |
PAGE_FREE | 2字节 | 各个已删除的记录通过next_record组成一个单链表,这个单链表中的记录所占用的存储空间可以背重新利用;PAGE_FREE表示该链表痛头节点对应记录在页中的偏移量,也就是已删除记录中的最小的那条记录的偏移量。 |
PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
PAGE_DIRECTION | 2字节 | 表示最新一条记录插入的方向,新插入的一条记录的主键值比上一条记录的主键值大,插入方向为右,反之则为左。 |
PAGE_N_DIRECTION | 2字节 | 同一个方向连续插入的记录数量,若改变方向,该状态值会清零后重新计数 |
PAGE_IN_RECS | 2字节 | 该页中用户记录的数量(不包括Infimum、Supremum和被删除的记录) |
PAGE_MAX_TRX_ID | 8字节 | 修改当前页的最大事务ID,仅在二级索引页面中定义 |
PAGE_LEVEL | 2字节 | 当前页在B+树中所处的层级 |
PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10字节 | B+树叶子结点段段头部信息,仅在B+树的根页面中定义 |
PAGE_BTR_SEG_TOP | 10字节 | B+树非叶子结点段的头部信息,仅在B+树的根页面中定义 |
File Header
File Header(文件头部)是页的第一部分,它固定占用38个字节,可以描述各种类型的页的一些通用信息。File Header数据构成如下:
状态名称 | 占用空间大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | MySQL 4.0.14前表示本页面所在的表空间ID,之后的版本表示页的校验和checksum |
FIL_PAGE_OFFSET | 4字节 | 页号 |
FIL_PAGE_PREV | 4字节 | 上一页的页号 |
FIL_PAGE_NEXT | 4字节 | 下一页的页号 |
FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的LSN值(Log Sequence Number,日志序列号) |
FIL_PAGE_TYPE | 2字节 | 该页的类型 |
FILE_PAGE_FILE_FLUSH-LSN | 8字节 | 仅在系统表空间的第一个页中定义,表示文件至少被刷新到了对应的LSN值 |
FILE_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
-
FIL_PAGE_SPACE_OR_CHKSUM:通过某种算法计算出来的较短的值用来比较不同的页是否相等,避免使用很长的字节去比较。两个校验和不想等,则两个长字节穿肯定不等。
-
FIL_PAGE_OFFSET:每一个页都有一个单独的页号,InnoDB通过页号来唯一定位一个页。
-
FIL_PAGE_TYPE:表示当前页的类型。除了存储数据的索引页,还有很多其他类型的页,如下表所示:
页的类型 | 十六进制 | 描述 |
---|---|---|
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还未使用 |
FIL_PAGE_UNDO_LOG | 0x0002 | undo日志页 |
FIL_PAGE_INODE | 0x0003 | 存储段的信息 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Change Buffer空闲列表 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | Change Buffer的一些属性 |
FIL_PAGE_TYPE_SYS | 0x0006 | 存储一些系统数据 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
FIL_PAGE_TYPE_XDES | 0x0009 | 存储区的一些属性 |
FIL_PAGE_TYPE_BLOB | 0x000A | 溢出页 |
FIL_PAGE_INDEX | 0x45BF | 索引页 |
前面说的溢出列,记录是存储在Fil_PAGE_INDEX(索引页),剩余数据存储在FIL_PAGE_TYPE_BLOB(溢出页)。
-
FIL_PAGE_PREV和FIL_PAGE_NEXT:存储数据记录的页可以组成一个双向链表,无须页在物理上相邻。页的相邻结构如下:
FIle Trailer
File Trailer(文件尾部)位于页面最末端,通用于所有类型的页,这部分由8字节组成,可分为2个小部分。由于每次读写数据时,需要以页为单位加载到内存中处理,文件尾部中这2个部分都是为了校验页的完整性。
-
前4个字节代表页的校验和。每当一个页在内存中发生修改时,在刷新前就要把页面的校验和算出来,File Header中的FIL_PAGE_SPACE_OR_CHKSUM也是干同样的一件事,File Header在页面的前边,所以File Header中的校验和会被先刷新到磁盘,当完全写完后,File Trailer中的校验和也会被刷新到磁盘。如果页面刷新成功,两个校验和应该是一致的。如果不一致,则代表刷新期间发生了错误,导致未能完全刷新磁盘。
-
后4个字节代表页面被最后修改时对应的LSN的后4字节,正常情况下应该和File Header中的FIL_PAGE_LSN的后4字节相同。
总结
页是InnoDB中磁盘和内存交互的基本单位,也是InnoDB管理存储空间的基本单位,默认大小为16KB。
InnoDB定义了4中行格式:
- COMPACT
-
REDUNDANT
-
DYNAMIC
与COMPACT类似,区别在于溢出列的处理方式有不同。DYNAMIC则是把所有数据都存储在溢出页中,在记录中只存储20个字节用来表示溢出页地址和数据占用字节数。
-
COMPRESSED
与DYNAMIC相同,不同的是COMPRESSED采用压缩算法对页面进行压缩。
InnoDB为了不同的目的设计了不同类型的页,用于存放记录的页称为索引页。一个索引页可以被大致划分为7个部分,分别如下:
-
File Header:表示页的一夜通用信息,固定占用38字节;
-
Page Header:表示数据页转悠的一些信息,固定占用56字节;
-
Infimum + Supremum:分别表示页中最小和最大的两条记录,固定占用26字节;
-
User Records:用户插入的记录,大小不固定;
-
Free Space:未使用的部分,大小不固定;
-
Page Directory:存放页中每个分组中最后一条记录的偏移量,也就是槽;
-
File Trailer:用于校验页的完整性,固定占用8字节。
阅读参考《MySQL是怎样运行的》小孩子4919。
标签:存储,字节,记录,占用,学习,InnoDB,MySQL,PAGE,FIL From: https://www.cnblogs.com/haleyeung/p/17740911.html