Mysql高级篇学习总结8:InnoDB数据存储结构页的概述、页的内部结构、行格式

1、数据库的存储结构:页

由于InnoDB是Mysql的默认存储引擎,所以本篇主要剖析InnoDB存储引擎的数据存储结构。

1.1 磁盘与内存交互的基本单位:页

InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB

以页作为磁盘与内存之间交互的基本单位,也就是说,不论读一行还是读多行,都是将这些行所在的页进行加载。因此数据库管理存储空间的基本单位是页(Page),数据库I/O操作的最小单位是页

1.2 页结构的概述

页a,页b,页c…页n,这些页可以在物理结构上不相连,只通过双向链表相关联即可。

每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里面的记录生成一个页目录,在通过主键查找某条记录的时候,可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录,即可快速找到指定的记录。

在这里插入图片描述

1.3 页的大小

不同数据库管理系统的页大小不同。

比如Mysql的InnoDB存储引擎中,默认页的大熊啊是16KB:

mysql> show variables like '%innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.21 sec)

SQL Server中页的大小是8KB。而Oracle中用术语“块”来代表“页”,Oracle支持的块的大小为:2KB, 4KB, 8KB, 16KB, 32KB, 64KB。

1.4 页的上层结构

在数据库中,还存在着区(Extent),段(Segment),表空间(Tablespace)的概念,它们之间的关系如下:
在这里插入图片描述

  1. 区(Extent)是比页大一级的存储结构。在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中页大小默认是16KB,所以一个区的大小是64 * 16KB = 1MB
  2. 段(Segment)是由一个或多个区组成。不要求区与区之间是相邻的,段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当创建数据表、索引的时候,就会相应创建对应的段。比如创建一张表时,会创建一个表段,创建一个索引时,会创建一个索引段。
  3. 表空间(Tablespace)是一个逻辑容器、表空间存储的对象是段。数据库是由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。

2、页的内部结构

页如果按类型划分的话,常见的有数据页(保存B+树节点)、系统页、Undo页、事务数据页等。数据也是我们最常使用的页。

数据页的16KB大小的存储空间被划分为七个部分,分别是:

  1. 文件头(File Header)
  2. 页头(Page Directory)
  3. 最大最小记录(Infimum + supremum)
  4. 用户记录(User Records)
  5. 空闲空间(Free Space)
  6. 页目录(Page Header)
  7. 文件尾(File Trailer)
    在这里插入图片描述

这7个部分的作用如下:
在这里插入图片描述

2.1 第1部分:File Header、File Trailer

首先介绍文件通用部分:文件头和文件尾

2.1.1 文件头:File Header

作用:描述各种页的通用信息(比如页的编号、上一页、下一页是谁等)
大小:38字节
在这里插入图片描述
1)FILE_PAGE_OFFSET:
每一个页都有一个单独的页号,InnoDB通过页号可以唯一定位一个页。
2)FILE_PAGE_TYPE:
这个代表当前页的类型
在这里插入图片描述
3)FILE_PAGE_PREV, FILE_PAGE_NEXT
分别代表本页的上一页和下一页的页号。保证这些页不需要在物理上连续,而是逻辑上连续就可以了。
在这里插入图片描述
4)FILE_PAGE_SPACE_CHKSUM
代表当前页面的校验和(checksum)。

什么是校验和?
就是对于一个很长的字节串俩说,通过某种算法来计算一个比较短的值来代表这个长字节串,这个比较短的值就是校验和。
它的作用是,可以先比较两个长字节串的校验和,如果校验和都不一样,那这2个长字节串肯定也是不同的,省去了直接比较两个长字节串的时间损耗。

文件头和文件尾都有该属性,它可以让数据库校验该页是否完整

InnoDB存储引擎是以页为单位把数据加载到内存中处理,然后将修改后的数据同步到磁盘中,如果此时同步了一半而断电或者其他原因的话,造成该页传输不完整。
那么InnoDB可以通过文件尾的校验和与文件头的校验和作比对,如果这2个值不相等,说明页的传输有问题,那么就需要进行相应地处理。
5)FILE_PAGE_LSN
页面被最后修改时对应的日志序列的位置(Log Sequence Number)

2.2 第2部分:Free Space、User Records、最大最小记录

2.2.1 空闲空间:Free Space

存储的记录会按照指定的行格式存储到User Records部分。

每当插入一条记录,都会从Free Space部分中申请一个记录大小的空间划分到User Records部分。当Free Space空间申请完后,如果还有新的记录要插入的话,就需要去申请新的页了
在这里插入图片描述

2.2.2 用户记录:User Records

User Records中的数据记录是按照指定的行格式一条一条地摆在User Records部分,相互之间形成了单链表。

接下来讲一下每一行记录行格式的记录头信息

2.2.2.1 行格式-记录头信息

首先还是新建一个表来进行演示说明。其中表名:page_demo,共3个字段。其中行格式使用的是Compact格式。
在这里插入图片描述

因此一条数据记录就以Compact行格式来进行保存,此时一条数据记录保存的信息如下。
在这里插入图片描述

下面主要说明的是行格式里面的记录头信息,看看它是怎么帮助mysql来记录用户的数据记录的。
记录头信息里面的属性如下:
在这里插入图片描述

简化后的行格式如下:
在这里插入图片描述
此时插入4条数据,记录头信息里面的情况如下图:
在这里插入图片描述
1)delete_mask
这个属性标记着当前记录是否被删除,占用1个二进制位:

  • 值为0:代表记录并没有被删除
  • 值为1:代表记录被删除了

那被删除的记录为什么还在页中存储呢
因为移除删除的记录的话,其他记录需要重新排列,导致性能消耗
所以只是打一个删除标记的话,所有被删除掉的记录都会组成一个所谓的垃圾链表。
在这个链表中的记录占用的空间称之为可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉
2)min_rec_mask
B+树的每层非叶子节点中的最小记录会添加该标签,min_rec_mask的值为1。
3)record_type
这个属性表示当前记录的类型,一共4种类型的记录:

  • 0表示普通用户数据记录
  • 1表示B+树非叶子节点记录
  • 2表示最小记录
  • 3表示最大记录

4)heap_no
这个属性表示当前记录在本页中的位置。

在上图中,可以看到,heap_no没有0和1的记录。因为这2个记录是由mysql插入的,有时间页称为伪记录或者虚拟记录。这2个记录,一个代表最小记录,另一个代表最大记录。
5)n_owned
页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段。
6)next_record
该属性表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。

注意,下一条记录指的并不是按照我们写入的顺序的下一条记录,而是按照主键值大小排序的下一条记录。

并且规定Infimum记录(最小记录)的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条就是Supremum记录(最大记录)。

如下图所示:
在这里插入图片描述

2.2.3 最小最大记录:Infimum + Supremum

在这里插入图片描述
在这里插入图片描述

2.3 第3部分:Page Directory、Page Header

2.3.1 页目录:Page Directory

为什么需要页目录?

在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索,提升效率。

使用页目录的二分查找的记录流程如下:

  1. 将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为”已删除“的记录。
  2. 第1组,也就是最小记录所在的分组只有1个记录。最后一组,也就是最大记录所在的分组,会有1-8条记录。其余的组的记录数量在4-8条之间。
  3. 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段
  4. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot)每个槽相当于指针指向了不同组的最后一个记录

举例如下图:
在这里插入图片描述

2.3.2 页头部:Page Header

为了得到一个数据页中存储的记录的状态信息。比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中粗糙黏糊了多少个槽等等,特意在页中定义了一个叫Page Header的部分这个部分占用固定的56个字节,专门存储各种状态信息。
在这里插入图片描述

3、行格式

平时的数据以行为单位向表中插入数据,这些记录在磁盘上的存放方式也被称为行格式或者记录格式

InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact, Dynamic, Compressed, Redundant 行格式,可以通过以下语句查看mysql的默认行格式:

mysql> select @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic                     |
+-----------------------------+
1 row in set (0.00 sec)

也可以查看具体表使用的行格式:

SHOW TABLE STATUS LIKE '表名'\G

3.1 Compact行格式

在mysql 5.1中,默认设置为Compact行格式。一条完整的记录可以被分为记录的额外信息和记录的真实数据的2大部分。
在这里插入图片描述

3.1.1 变长字段长度列表

mysql支持一些变长的数据类型,比如VARCHAR(M), VARBINARY(M), TEXT, BLOB类型,这些数据类型修饰列为称为变长字段,变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。

在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部分,从而形成一个变长字段长度列表。

举例如下图:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.1.2 NULL值列表

Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值的列表中。如果表中没有允许存储NULL的列,则NULL值列表也就不存在了。

  • 二进制位的值为1时,代表该列的值为NULL;
  • 二进制位的值为0时,代表该列的值不为NULL;

举例如下图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.1.3 记录头信息

记录头信息在上面(2.2.2.1 行格式-记录头信息)详细介绍过,请参考上面部分的内容。

3.1.4 记录的真实数据

记录的真实数据除了我们自己定义的列的数据之外,还会有3个隐藏列:
在这里插入图片描述

3.2 Dynamic和Compressed行格式

3.2.1 行溢出

Innodb存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外

由于varchar类型最多可以存储65535个字节,而且还要除去保存变长字段列表的2字节,NULL值列表的1字节的话,那么可以创建varchar为65533字节的字段。

CREATE TABLE varchar_size_demo(
    c VARCHAR(65533) not null
) CHARSET=ascii ROW_FORMAT=Compact;

由于一个页的大小一般是16KB,也就是16384个字节,因此一个页连一行记录都放不下,这种现象就称为行溢出

3.2.2 Dynamic和Compressed行格式

针对上面的行溢出现象。

在Compact和Redundant行格式中,在记录真实数据处,只会存储该列的一部分数据,把剩余的数据分散存储在几个其他页中进行分页存储,然后再记录的真实数据处,用20个字节存储指向这些页的地址,从而可以找到剩余数据所在的页。
在这里插入图片描述

而Compressed和Dynamic两种记录格式对于存放在blob的数据采用了完全溢出的方式。在数据页中只存放20个字节的指针,实际的数据都存放在off Page(溢出页)中
在这里插入图片描述

Compressed行格式的另一个功能是,存储在其中的行格式会以zlib的算法进行压缩,因此对于blob, text, varchar这类大长度类型的数据能够进行非常有效的存储。

4、区、段、碎片区

4.1 为什么要有区?

B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远,此时查找数据是随机I/O。所以如果能让相邻页的物理位置也相邻,这样进行范围查询的时候可以使用所谓的顺序I/O。这样查询的速度快很多!

引入的概念,一个区就是在物理位置上连续的64个页。因为Innodb的页的大小是16KB,所以一个区的大小是64*16KB=1MB。

在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配。深圳在表中的数据特别多的时候,可以一次性分配多个连续的区,虽然可能造成一点点空间的浪费(如果数据不能填充满整个区)。但是从性能的角度看,可以消除很多随机I/O,功大于过

4.2 为什么要有段?

上一篇中介绍了怎么通过B+树来找到最终的用户记录数据,由于用户记录数据最终是存储在叶子节点上的。所以如果不区分叶子节点和非叶子节点,全部都放到一个区里的话,那么效果就大打折扣了,因为叶子节点可能还是不是连续的

所以Innodb对叶子节点和非叶子节点进行了区别对待,它们都有各自的区。存放叶子节点的区的集合就算是一个段(Segment)。存放非叶子节点的区的集合页算是一个段。

除了索引的叶子节点段和非叶子节点段之外,Innodb中还有为存储一些特殊的数据而定义的段,比如回滚段。所以,常见的段有数据段、索引段、回滚段。数据段即B+树的叶子节点,索引段即B+树的非叶子节点。

4.3 为什么要有碎片区?

默认情况下,一个使用Innodb存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M(64 * 16KB = 1024KB)存储空间。如果一张表只是存储了很少的几条数据,如果页占用2M空间的话,就有点浪费了

针对这个问题,Innodb提出了一个**碎片区(fragment)**的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的。比如有些页用于段A,有些页用于段B,有些页甚至那个段都不属于。碎片区直属于表空间,并不属于任何一个段。

所以此后为某个段分配存储空间的策略如下:

  1. 在开始向表中插入数据的时候,段从某个碎片区以单个页面为单位来分配存储空间;
  2. 当某个段已经占用了32个碎片区页面之后,就会申请以完整的区为单位来分配存储空间

4.4 区的分类?

区大体上可以分为4种类型:

  • 空闲的区(FREE):现在还没有用到这个区中的任何页面
  • 有剩余空间的区(FREE_FRAG):表示碎片区中还有可用的页面
  • 没有剩余空间的区(FULL_FRAG):表示碎片区中所有页面都被使用,没有空闲页面
  • 附属于某个段的区(FSEG):每个索引都可以分为叶子节点段,和非叶子节点段

处于FREE, FREE_FRAG, FULL_FRAG 这3种状态的区都是独立的,只属于表空间,而处于FSEG装填的区是附属于某个段的。

5、表空间

表空间是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。表空间数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间(System tablespace)、独立表空间(File-per-table tablespace)、撤销表空间(Undo Tablespace)、临时表空间(Temporary Tablespace)等。

5.1 独立表空间

独立表空间,即每张表都有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立表空间可以在不同的数据库之间进行迁移。

一个新建的表对应的.ibd文件只占用了96K(6个页面的大小)。这是因为一开始表空间占用的空间很小,因为表里没有数据。随着表中数据的增多,表空间对应的文件页逐渐增大。

猜你喜欢

转载自blog.csdn.net/xueping_wu/article/details/125469409