数据库的存储结构:页

  • 索引为我们提供了高效的查找方式,不过索引信息以及数据记录都是保存在文件上的,确切说是存储在页结构中的。
  • 索引是在存储引擎中实现的,不同存储引擎中存放的格式一般是不同的。
  • 本章以InnoDB存储引擎为基础来分析数据存储结构。

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

  • 页是磁盘和内存之间交互的基本单位。在数据库中,不论一次读取一行还是多行,都是加载整个页,一个页中可以存储多条数据,数据库IO操作的最小单位是页。同时,如果只修改页中的一条数据,落盘的时候也是按照页为单位进行的。
  • InnoDB将数据划分为若干个页,每个页的大小默认是16KB;

可以通过show variables like ‘%innodb_page_size’;查看
image.png

  • 页结构

页之间是通过双向链表逻辑相连的。
页中的数据会按照主键值从小到大的顺序组成一个单向链表(聚簇索引)
image.png

  • 页的上层结构:区(Extent)、段(Segment)和表空间(Tablespace)的概念,关系如下

image.png
是比页大一级的单位,在文件系统中是一个连续的空间。在InnoDB中,一个区中有64个连续的页,所以一个区大小为64×16=1MB
是一个或多个区组成的,段中不要求区与区之间是连续的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建表、索引的时候,就会创建相应的段。
表空间是一个逻辑容器,表空间存储的对象是段,在一个表空间中有可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或者多个表空间组成,表空间从管理上可以分为系统表空间、用户表空间、撤销表空间、临时表空间。

页的内部结构

  • 页按照类型划分,常见的有数据页(B+树节点)、系统页、Undo页、事务数据页等。
  • 数据页16KB大小被划分为7个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+Supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)。

image.png

文件头和文件尾

  • 文件头:描述页的信息

38个字节,描述页的通用信息
image.png
页号FIL_PAGE_OFFSET:InnoDB通过页号唯一定位一个页
页类型FIL_PAGE_TYPE:
image.png
上一页和下一页FIL_PAGE_PREV(4字节)和FIL_PAGE_NEXT(4字节):因为页与页之间是双向链表的形式进行连接的
image.png
校验和FIL_PAGE_SPACE_OR_CHKSUM(4字节):文件头和文件尾都存在校验和。当向磁盘存页的时候,首先修改文件头的校验和,当页保存完毕,修改文件尾的校验和为文件头的校验和。取的时候也需要判断头尾校验和一致才是一个完整的页。避免同步过程中出现问题
FIL_PAGE_LSN(8字节):LSN,Log Sequence Number。页面最后被修改对应的日志序列的位置,如果文件头和文件尾的LSN不一致,也说明同步出现问题

  • 文件尾:校验页是否完整
    • 校验和
    • LSN

用户记录、最大最小记录、空闲空间

  • 用户记录:

image.png
用户记录中的每一个记录都是按照指定的行格式摆在其中的,相互之间形成单链表
用户记录的单链表如何形成的,行格式又是什么结构的,跳转到Compact行格式记录头信息

  • 最大最小记录

如何比较记录的大小?其实就是比较记录的主键的大小,详细的记录头信息见行格式中的heap_no
image.png

  • 空闲空间:我们自己存储的记录会按照指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

页目录、页面头部

  • 页目录

每个页中记录的条数很多,记录与记录之间是通过单链表的形式进行连接。如何快速定位到记录?
页目录就是为了快速定位页中记录的。其中存储的是组中最后一条记录的地址偏移量。
image.png
image.png
如何通过页目录快速查找记录?
首先,通过B+树找到对应的页,进入到页面之后,通过页目录使用二分法定位到对应的槽,然后从槽中查找数据。

  • 页面头部

记录页中存储的记录的状态信息,比如本页存储的记录条数,第一条记录的地址,页目录中有多少个槽等等,长度56字节
image.png

B+树的角度看页的内部结构

image.png

InnoDB行格式

InnoDB中默认行格式

  • 查看存储引擎默认行格式

SELECT @@innodb_default_row_format;
MySQL8.0.26默认是dynamic,同时MySQL5.7也是dynamic

  • 查看表的行格式

SHOW TABLE STATUS LIKE ‘表名’
image.png
在创建表的时候可以显式指定行格式:CREATE TABLE emp(id int ,name VARCHAR(20)) row_format=compact;
当然也可以修改表的行格式:ALTER TABLE emp row_format=compact;

Compact行格式

  • 结构示意图

记录额外信息+记录的真实数据
image.png

  1. 变长字段长度列表

记录可变长度的字段的实际长度。(顺序为表字段的倒序)
比如表(id int , name varchar(20),address varchar(1000)),插入记录(1,’zhangsan’,’beijing’)
那么这一行的边长字段长度列表就是78,分别表示beijing,zhangsan的长度。

  1. NULL值列表

将NULL值的列统一管理起来,如果表字段存在可以存放NULL值的列的话。
当然,如果某一行没有NULL值,就没有这个字段了。
当一个字段明确指明为NOT NULL类型的,NULL值列表中不会记录
也是按照表字段的倒序进行排列,0表示为不为NULL,1表示为NULL
image.png

  1. 记录头信息(5字节)

image.png
其中各个属性如下
image.png
简化后的行格式如下(演示方便)
image.png
插入4条数据如下(10进制展示)
image.png
Delete_mask:这个属性标记着当前记录是否被删除,占用1个二进制位。值为0:代表记录并没有被删除值为,1:代表记录被删除掉了
为什么被删除的记录还在页中存储?
因为记录之间是紧密排列的,移除之后好需要对后面的数据重新排列,导致性能损耗。这里的处理方法是将删除的记录之间通过链表连接,组成一个垃圾链表。占用的空间称为可重用空间,后续如果有新的记录,可以直接覆盖掉。
min_rec_mask:B+树的每层非叶子节点中的最小记录都会添加该标记,min_rec_mask值为1。我们自己插入的四条记录的min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录。
record_type:记录类型
image.png
heap_no:表示当前记录在本页中的位置
image.png
最小记录和最大记录信息是存放在页中的(非用户记录中)
image.png
n_owned:页目录每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段。详情见page directory
next_record:记录头信息里该属性非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
image.png
删除演示
image.png

  1. 记录的真实数据

除了真实数据之外,还会有3个隐藏列
image.png

Dynamic和Compressed行格式

  • 和Compact比较相似,在行溢出的处理上不同
  • 行溢出

InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。
MySQL对于可变长度的字段,最长是65535字节,但是由于可变长度2个字节,NULL值长度1个字节之外,最多可以存放65532字节长度,当然如果字段都是NOT NULL,最多可以定义的字段长度是65533字节
而一个页的大小是16KB,16384字节。而一个VARCHAR(65532)最多可以存储65532字节,出现一个页都存不下一条记录,这种现象就成为行溢出。
image.png

  • 和Compact的溢出处理方式的区别

image.png

Redundant行格式

  • Redundant是MySQL 5.0版本之前InnoDB的行记录存储方式,MySQL 5.0支持Redundant是为了兼容之前版本的页格式。

image.png
字段长度偏移列表会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。

区、段与碎片区

  1. 为什么要有区?
    • 对于范围查询来说,只需要知道最左边和最右边的页面即可,然后沿着双向链表依次加载页面进内存。存在的问题是:如果页面之间距离较远,那么磁盘IO(随机IO)将要比按照顺序IO(顺序IO)花费更长的时间。
    • 由此,引入区的概念,一个区在物理磁盘上是64个连续的页。当表中数据量很大的时候,为索引分配空间不再按照页为单位分配,而是按照区为单位进行分配。甚至当数据足够大的时候,可以一次性分配做个连续的区,避免随机IO,虽然可能空间用不完。
    • 需要注意的是区与区之间并不一定是物理上连续的。
  2. 为什么要有段
    • 段中存放一个或者多个区。由于区中存放的页可能是数据页也可能是目录页。当我们想要定位某个数据页的时候,需要连续加载目录页找到对应的数据页。
    • 连续加载目录页,当然顺序IO最好。所以,InnoDB对于叶子结点和非叶子结点进行了划分,分为存放叶子结点的区和存放非叶子结点的区,这里的区的集合就是段。这样当我们从磁盘加载目录页的时候就可以顺序IO(当然了,这些目录页在同一个区)了。
    • 存放叶子结点的称为叶子节点段,存放非叶子结点的称为非叶子节点段。此外还有数据段(B+树叶子结点),索引段(B+树非叶子结点),回滚段
    • InnoDB中对于段的管理是存储引擎自身完成的。
    • 段是逻辑上的概念,并不是表空间中的一个连续的物理区域。由若干个零散的页面(?)以及一些完整的区组成
  3. 碎片区
    • 默认情况下,InnoDB存储引擎只有一个聚簇索引。按照之前的设定来看,一个索引需要两个段,分别为数据段和索引段。段是以区为单位来申请的,也就是说,即使只有一条记录,也会占用2个区(2MB大小)。显然会造成空间的浪费
    • 为了避免上述分配空间的情况,InnoDB提出了碎片区的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在。碎片区中的页可以用于不同的目的,可以属于任意一个段甚至不属于任意一个段。碎片区直属于表空间
    • 有了碎片区之后,空间分配策略如下:
      • 刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
      • 某个段已经占用32个碎片区之后,就会申请完整的区作为存储空间。
    • 因此,段定义为由若干个零散的页面以及一些完整的区组成
  4. 区的分类
    1. 空闲区:区中的页还未被使用
    2. 有剩余空间的碎片区:碎片区中有可用的页
    3. 没有剩余空间的碎片区:碎片区中没有可用的页
    4. 附属于某个段的区:比如只属于叶子结点段的区和只属于非叶子结点段的区。

前3种区直属于表空间,最后一种区属于某个段。

表空间

  • InnoDB存储引擎逻辑结构的最高层,所有的数据都存储在表空间中
  • 表空间是一个逻辑容器,表空间存储的对象是段,在一个表空间中有可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或者多个表空间组成,表空间从管理上可以分为系统表空间、用户表空间、撤销表空间、临时表空间。

独立表空间

  • 也即每张表都有一个独立的表空间。所有的数据和索引都存储在自己的表空间中。
  • 独立的表空间可以在不同和数据库之间进行迁移。通过DROP TABLE可以对表空间进行回收
  • 由段、区、页组成
  • 真实表空间对应的文件(MySQL8.0.26)为.ibd,是自扩展的,随着表中数据的增加,表空间对应的文件也在组件增大。
  • 查看InnoDB表空间类型:

image.png

系统表空间

  • 整个MySQL服务器只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页,这部分在独立表空间是没有的。
  • InnoDB数据字典:为了更好地管理用户数据而不需要引入额外的数据,这些额外的数据就存储在系统表空间中。这些表称为InnoDB数据字典

image.png
image.png
image.png
image.png
image.png
image.png