MySQ高级十二:InnoDB的数据存储结构 - 页(详解)

InnoDB的数据存储结构 - 页

引入
  1. 索引结构为我们提供了高效的索引方式,而索引信息以及数据记录都是保存在文件上。

  2. 索引是在存储引擎中实现的,在不同的存储引擎中,索引和数据的保存的文件形式是不同的。

  3. MySQL中,默认的存储引擎是InnoDB,所以接下来重点研究索引在InnoDB中的存储形式:页

  4. 所以为了深入理解索引的工作原理,就需要剖析数据页结构

  5. 页的内部结构与行格式的对应

    页结构(16kB)compact行格式
    1User Records记录头信息
    2Infimum+Supremum(最大最小记录)heap_no
    3Page Directory (页目录)o_owned
一、页
  1. 简介

    页,是磁盘与内存之间交互的基本单位,且InnoDB中页的默认大小为16kB,即从磁盘中做一次IO操作(读取数据到内存中,或从内存中把数据刷新到磁盘中),最少是16kB的数据。

    一个页中可以存储多个行记录,但是在数据库中不论多一行,还是多行,都会将行所在的页全部加载。

  2. 页结构

    ① 数据页内的记录会按照主键值从小到大的顺序组成单向链表;

    ② 在单向链表中查询数据,需要遍历所有记录,影响查询效率。为此在数据页内会为存储在它里边的所有记录生成一个页目录,这样通过主键查找某条记录的时候就可以在目录中使用二分法快速定位到对应的槽,然后再遍历该槽中对应分组中的记录即可快速找到指定的记录。(极其重要的关键结构

    ​ 页内记录 -> 页目录 -> 槽 -> 槽内分组记录

    ③ 页与页之间不在物理结构上相连,通过双向链表进行关联。

    在这里插入图片描述

二、页的上层结构
  1. 数据整体存储结构

    在这里插入图片描述

  2. Extent,比页大一级的存储结构,且是一个连续分配的空间。在InnoDB存储引擎中,一个区会分配64个连续的页。所以一个区的大小是64 x 16kB = 1MB。

  3. Segment,由多个区组成,不过段中的区与区之间可以是不相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段

  4. 表空间

    是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以由一个或多个段组成,但是一个段只能属于一个表空间。

    同时表空间也是能接触到的数据存储的最大概念,因为表空间已经是在操作系统中实际存在的表,且数据库是由一个 或多个表空间组成。

  5. InnoDB下的数据目录

    表结构:InnoDB在数据目录下对应的数据库子目录下创建了一个专门用于描述表结构的.frm文件

    系统表空间:InnoDB在数据目录下创建了一个名为ibdata1,大小为12M,且大小可以子扩展的文件

    独立表空间:MySQL5.6后,InnoDB为每一个表建立一个独立表空间.ibd文件

三、页的内部结构
  1. 页的分类

    常见有数据页(保存B+树的节点,包括叶子节点与非叶子节点)、系统页、Undo和事务数据页

  2. 数据页的内部划分

    16kB的存储空间被划分为七个部分,分别是文件头、页头、最大最小记录、用户记录、空闲空间、页目录和文件尾。

    在这里插入图片描述

    这7个结构也可分为3个部分

    第1部分:File Header(文件头部)和 File Trailer(文件尾部);

    第2部分:是存储记录部分,包括最大最小记录、用户记录、空闲空间;

    第3部分:页目录、页面头部。

  3. File Header(文件头部)

    作用:描述各种页的通用信息,比如页的编号、上下页是谁等

    包含内容(38字节):

    名称大小作用
    FIL_PAGE_SPACE_OR_CHKSUM4B页的校验或checksum值
    FIL_PAGE_OFFSET4B页号,InnoDB通过页号确定一个页
    FIL_PAGE_PREV4B上一页的页号
    FIL_PAGE_NEXT4B下一页的页号
    FIL_PAGE_LSN8B页面被最后修改后对应日志序列位置
    Log Sequence Number
    FIL_PAGE_TYPE2B页的类型
    FIL_PAGE_FILE_FLUSH_LSN8B仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
    FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID4B页属于哪个表空间

    重要的几个内容

    ① 校验和(FIL_PAGE_SPACE_OR_CHKSUM)

    ​ 比较两个很长的字符串,可以通过某种算法来分别计算出两个较短的值代表两个字符串,计算出的较短的值就是校验和。如果校验和相等,那么两个很长的字符串也相同。同理,比较两个页面是否相同,也可以使用校验和。

    ​ 因此,在文件头和文件尾均设有校验和属性,如果一个页面在内存中被修改,而在同步到磁盘过程中出现故障而导致同步失败,就会造成页传输的不完整,这时只要对比页头和页尾的校验和的值即可(因为校验和的属性在页头,所以同步的时候肯定先比真实记录被刷新到磁盘中,这就保证了比较页头和页尾的校验和确实可以验证页面是否传输完整)。

    ② File_PAGE_LSN

    ​ 表示页面被最后修改时对应的日志序列位置,也是为了校验页的完整性。

    ③ 页的类型

    在这里插入图片描述

  4. File Trailer(文件尾部)

    8个字节,就包含FIL_PAGE_SPACE_OR_CHKSUM 和 FIL_PAGE_LSN两个属性,都是为了校验页面传输是否完整。

  5. User Records(用户记录)

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

    叶子节点中存放的是一条条真实插入的记录,非叶子节点存放的是目录项(参考:索引

    至于记录中的一条条数据是如果被记录的,如何形成单链表,具体实现在行格式中详细了解

  6. Infimum Supremum(最小最大记录)

    ① 最小记录和最大记录由5个字节大小的记录头信息和8个字节大小的固定部分组成

    在这里插入图片描述

    ② 与User Records中的存储状况是不同的(详见记录头信息中的heap_no)

    在这里插入图片描述

  7. Page Directory(页目录)

    描述:页目录中存储的是用户记录的相对位置

    功能:页中的记录存储使用的是单向链表方式,传统的查询方式只能是顺序查找,效率低。为了提高效率,就使用了页目录,从而可以使用二分法进行查询。

    功能实现如下:

    ① 将所有的记录分为几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录

    ② 第1组:只包含最小记录

    ​ 最后1组:有1-8条记录,包含最大记录。

    ​ 其它组:记录数在4 - 8 条之间

    ③ 分组步骤:

    ​ 首先,初始情况下一个数据页中只有最小记录和最大记录两条记录,它们分属于两个分组。

    ​ 之后,每插入一条记录,都会从页目录中找到主键值比该记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个

    ​ 另外,当一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分为两个组,一个组中4条记录,另一个组中5条记录。这个过程会在页目录中新增一个槽记录这个新增分组中最大的那条记录的偏移量。

    ④ 每个小组中的最后一条记录的记录头信息中会存储该组一共有多少条记录,作为n_owned字段

    ⑤ 页目录用来存储每组最后一条记录的偏移地址量,这些地址偏移量会按照先后顺序存储起来,每组的地址

    偏移量也被称之为槽(slot),每个槽相当于指针指向了对应组的最后

    注意:地址偏移量是从页面的 0 字节开始数。

    在这里插入图片描述

    18条记录的页目录

    在这里插入图片描述

  8. Page Header(页面头部)

    使用了56个字节,专门存储有关于该页的一些信息

    在这里插入图片描述

四、行格式
  1. 行格式种类

    ① COMPACT行格式

    ② Dynamic 和 Compressed行格式,MySQL8.0中,默认行格式是Dynamic。

    ③ Redundant行格式

  2. 查看MySQL中使用的行格式d

    在这里插入图片描述

  3. 更改数据表的行格式
    CREATE TABLE 表名(列信息) ROW_FORMAT=行格式名称;
    ALTER TABLE 表名 ROW_FORMAT=行格式名称;
    
五、COMPACT行格式
  1. 创建表

    # 创建数据表
    CREATE TABLE page_demo (c1 int, c2 int, c3 varchar(10000), primary key (c1))
    CHARSET = ASCII row_format = compact;
    
    # 在数据表中插入数据
    INSERT INTO page_demo VALUES
    (1, 100, 'song'),
    (2, 200, 'tong'),
    (3, 300, 'zhan'),
    (4, 400, 'lisi');
    
  2. COMPACE行格式的示意图

    在这里插入图片描述

  3. 变长字段列表

    变长字段类型有:VARCHAR(m)、VARBINARY(m)、TEXT类型。

    变长字段列表会将该条记录中变长字段的实际存放长度以进行存储,但是其长度和字段的顺序是反过来的。

  4. NULL值列表

    ① 使用

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

    ② 为什么要有NULL值列表

    原因一:数据存储都是需要对齐得,如果没有标注出NULL值得位置,就有可能在查询数据的时候出现混乱。

    原因二:如果使用一个特定的符号放在相应的数据位表示NULL的话,又会浪费空间

    ③ 存储方式

    1:代表该字段是NULL;0:代表该字段不是NULL;如果该字段是NOT NULL类型的,则不会标记。

    举例如下:

    在这里插入图片描述

  5. 记录的真实数据部分

    除了自己定义的列的数据外,还有三个隐藏列

    列名是否必须占用看空间描述
    DB_ROW_ID6个字节行ID,唯一标识一条记录
    DB_TRX_ID6个字节事务ID
    DB_ROLL_PTR7个字节回滚指针

    DB_ROW_ID:

    一个表中如果没有手动定义主键,则会选取一个Unique键作为主键,如果Unique键都没有定义的话,则会位表默认添加一个名为DB_ROW_ID的隐藏列作为主键。

    所以:该隐藏列只会在主键及Unique键都没有的时候才会存在

    注:事务ID和回滚指针在《MySQL事务日志》中详解

  6. 记录头信息示意图

    在这里插入图片描述

  7. 记录头各属性的含义

    在这里插入图片描述

  8. delete_mask

    删除标记位,0代表没有被删除,1代表被删除掉了

    但是删除后的记录仍然真实的存储在磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后,其它的记录在磁盘上需要重新排列,导致性能消耗。所以只是打一个删除标记而已,而所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间被称为可重用空间。之后如果有新纪录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。

  9. heap_no

    表示当前记录在本页中的位置,用户记录的位置是从2开始的

    0和1是为最小最大记录预留的,因为MySQL会自动给每个页中加两个记录,也被称为伪记录或虚拟记录,0位置的是最小记录,1位置的是最大记录

  10. next_record

    表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量

    注意:下一条记录指的并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且Infimum记录的下一条记录是主键值最小的用户记录,主键最大的用户记录的下一条记录就是Supremum记录。

    在这里插入图片描述

    如果删除了第2条记录,next_record会发生如下改变

    在这里插入图片描述

六、行格式之间的区别
  1. 行溢出

    在MySQL中,varchar的字段设置的长度单位是字符,最多可存65535个字节,即64kB。

    理论上:如果是以ASCII类型,则最多可以存储65535个字符;如果是utf-8,则最多可以存储21834个字符。

    实际中:在compact行格式中,还需要有2个字节记录变长字段长度列表,1个字节记录NULL值列表。

    在InnoDB存储引擎中,一个从compact行格式的角度出发,一个有16kB大小,所以这样就可能出现一个页放不了一条记录,这种现象称之为行溢出

    解决方式:

    在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储几个其他的页中进行分页存储,然后记录的真实数据处用20个字节存储指向这些页的地址及其他页面中数据占用的字节数等信息,从而可以找到剩余数据所在的页,这称之为页的扩展

  2. 各种行格式之间的异同体现在对行溢出的处理方式上

    ① Dynamic、Compressed对于存放在BLOB中的数据采用了完全的行溢出方式。在数据页中只存放了20个字节的指针,实际的数据都存放在了Off Page(溢出页)中。

    在这里插入图片描述

    ② Compact和Redundant两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)

    在这里插入图片描述

参考资料
  1. 为什么要有区

    我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据。而B+树的每一层中的页都会形成一个双向链表,如果是以为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离的非常远。我们介绍B+树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离的非常远,就是所谓的随机IO。再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机IO非常慢,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序IO。

    引入区的概念,一个区就是在物理位置上连续的64个页,大小为1MB。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区的单位分配,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点空间的浪费(数据不足以填充整个区),但是从性能角度看,可以消除很多的随机IO,功大于过!

  2. 为什么要有段

    我们提到的范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以InnoDB对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点由自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就是一个段,存放非叶子节点的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段

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

    在InnoDB存储引擎中,对段的管理都是由引擎自身所完成DBA不能也没有必要对其进行控制。这从一定程度上简化了DBA对于段的管理

    段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。

  3. 为什么要有碎片区

    默认情况下,一个使用InnoDB存储引擎的表只有以一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1MB存储空间,所以默认情况下一个只存了几条记录的小表也需要2MB的存储空间吗?以后每次添加一个索引都要多申请2MB的存储空间吗?这对于存储记录表较少的表简直是天大的浪费。InnoDB当然也考虑到这种情况,这个问题的症结在于到现在为止我们介绍的区都非常纯粹,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作它用。

    为了考虑以完整的区为单位分配给某个段对于数量较小的表太浪费存储空间的鄂这种情况,InnoDB提出了一个碎片区(fragment)的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段

    故,此后为某个段分配存储空间的策略是这样的

    ① 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的

    ② 当某个段已经占用了32个碎片区页面之后,就会申请以完整的单位来分配存储空间。

    所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面以及一些完整的区的集合

  4. 区分为4类

    空闲的区(FREE):现在还没有用到这个区的任何页面

    有剩余空间的碎片区(FREE_FRAG):表示碎片区中还有可用的页面

    没有剩余空间的碎片区(FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面

    附属于某个段的区(FSEG):每个索引都可以分为叶子节点段和非叶子节点段

    处于FREE,FREE_FRAG,FULL_FRAG这三种状态的区是独立的,直属于表空间。而处于FSEG状态的区是附属于某个段的

    如果把表空间比作是一个集团军,段就相当于师,区就相当于团。一般的团都是隶属于某个师的,就像是处于FSEG的区全都隶属于某个段,处于FREE,FREE_FRAG,FULL_FRAG这三种状态是直属于表空间,就像独立团听命于军部一样。

  5. 独立表空间

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

    方便回收,可以通过删除表,也可以通过alter table TableName engine=InnoDB;回收不用的空间。

    真实的独立表空间.ibd文件只占用96kB,6个页面大小,但是文件是自扩展的。

  6. 系统表空间

    其结构与独立表空间基本类似,只不过整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,这时独立表空间所没有的。

    InnoDB数据字典

    每当我们向一个表中插入一条记录时,MySQL检验过程如下:

    在这里插入图片描述

    在这里插入图片描述

    这些系统表就被称为数据字典。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

e_nanxu

感恩每一份鼓励-相逢何必曾相识

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值