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

索引结构给我们提供了高效的索引方式,不过索引信息以及数据记录都是保存在文件上的,确切说是存储在页结构中。另一方面,索引是在存储引擎中实现的,MySQL服务器 上的存储引擎负责对表中数据的读取和写入工作。
不同存储引擎中存放的格式一般是不同的,甚至有的存储弓|擎比如Memory都不用磁盘来存储数据。

由于InnoDB是MySQL的默认存储引擎,所以本章剖析InnoDB存储引擎的数据存储结构。

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

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

作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内
存中的16KB内容刷新到磁盘中。也就是说,在数据库中,不论读一行, 还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页(Page) ,数据库I/O操作的最小单位是页。一个页中可以存储多个行记录。

记录是按照行来存储的,但是数据库的读取并不以行为单位,否则- -次读取(也就是一次1/O操作)只能处 理一-行数据,效率会非常低。

image.png

1.2页结构概述

页a、页b、页c ..页n这些页可以不在物理结构上相连,只要通过双向链表相关联即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主
键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可
快速找到指定的记录。

1.3页的大小

不同的数据库管理系统(简称DBMS )的页大小不同。比如在MySQL的InnoDB存储引擎中,默认页的大小是
16KB,我们可以通过下面的命令来进行查看:

  1. mysql> show variables like '%innodb_page_size%';

image.png
SQL Server中页的大小为8KB,而在Oracle中我们用术语“”(Block) 来代表“页”,Oralce 支持的块大小为
2KB,4KB, 8KB,16KB,32KB和64KB。

1.4页的上层结构

另外在数据库中,还存在着区(Extent) 、段(Segment) 和表空间(Tablespace) 的概念。
行、页、区、段、表空间的关系如下图所示:
image.png
区(Extent) 是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中
的页大小默认是16KB,所以一个区的大小是6416KB= *1MB

段(Segment) 由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),
不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一 个表段, 创建一个索引时会创建一个索引段。

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

2. 页的内部结构

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

数据页的16KB大小的存储空间被划分为七个部分,分别是文件头(File Header)、页头(Page Header)、最大
最小记录(Infimum+supremum) 、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)。

页结构的示意图如下所示: .
image.png
这7个部分作用分别如下,我们简单梳理如下表所示:
image.png
我们可以把这7个结构分成3个部分。

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

首先是文件通用部分,也就是文件头文件尾

①文件头部信息

不同类型的页都会以File Header 作为第一个组成部分, 它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁等,所有的数据页会组成一个双链表。这个部分占用固定的38个字节,是由下边这些内容组成的:
image.png
其中:

  • FIL_PAGE_OFFSET

每一个页都有-个单独的页号,就跟你的身份证号码一样,InnoDB通过页号可以唯一定位一个页。

  • FIL_PAGE_TYPE

这个代表当前页的类型。InnoDB为 了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录
数据页,其实还有很多别的类型的页,具体如下:
image.png
我们存放记录的数据页的类型其实是FIL_PAGE_INDEX, 也就是所谓的索引页

②数据页的链接实现

在文件头部内容中有两个属性: FIL_PAGE_PREVFIL_PAGE_NEXT

InnoDB都是以页为单位存放数据的,如果数据分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREVFIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,保证这些页之间不需要是物理上的连续,而是逻辑上的连续。
image.png
image.png
③检验页的完整性

InnoDB存储引擎以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某
个时间需要把数据同步到磁盘中。但是在同步了一半的时候断电了,造成了该页传输的不完整。

为了检测一个页是否完整(也就是在同步的时候有没有发生只同步-半的尴尬情况), 这时可以通过文件尾的

第2部分: User Records (用户记录)、最大最小记录Free Space(空闲空间)

第3部分: Page Directory (页目录)、Page Header (页面头部)

2.3从数据页的角度看B+树如何查询

一棵B+树按照节点类型可以分成两部分:

  1. 叶子节点,B+树最底层的节点,节点的高度为0,存储行记录。
  2. 非叶子节点,节点的高度大于0,存储索引键和页面指针,并不存储行记录本身。

image.png
当我们从页结构来理解B+树的结构的时候,可以帮我们理解一些通过索弓|进行检索的原理:

1.B+树是如何进行记录检索的?

如果通过B+树的索引|查询行记录,首先是从B+树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(slot) 采用二分查找的方式先找到一个粗略的记录分组,
然后再在分组中通过链表遍历的方式查找记录。

2.普通索引和嘴一索引在查询效率.上有什么不同?

我们创建索弓|的时候可以是普通索引,也可以是唯一索引,那么这两个索弓|在查询效率上有什么不同呢?

唯一索弓|就是在普通索引上增加了约束性,也就是关键字唯一, 找到了关键字就停止检索。而普通索引,可能会
存在用户记录中的关键字相同的情况,根据页结构的原理,当我们读取一条记录的时候, 不是单独将这条记录从
磁盘中读出去,而是将这个记录所在的页加载到内存中进行读取。InnoDB 存储引擎的页大小为16KB,在一个页
中可能存储着上千个记录,因此在普通索引的字段上进行查找也就是在内存中多几次“判断下一条记录”的操作,
对于CPU来说,这些操作所消耗的时间是可以忽略不计的。所以对一个索引字段进行检索,采用普通索弓|还是唯
一索弓|在检索效率上基本上没有差别。

3. InnoDB行格式(或记录格式)

我们平时的数据以行为单位来向表中插入数据,这些记录在磁盘上的存放方式也被称为行格式或者记录格式
InnoDB存储引擎设计了4种不同类型的行格式,分别是CompactRedundantDynamic Compressed行格
式。查看MySQL8的默认行格式:

3.1指定行格式的语法

3.2 COMPACT行格式

1.变长字段长度列表

2. NULL值列表

3.记录头信息

4.记录的真实数据

3.3 Dynamic和Compressed行格式

1.行溢出

2. Dynamic和Compressed行格式

3.4 Redundant行格式

1.字段长度偏移列表

2.记录头信息(record header)

4.区、段与碎片区

4.1为什么要有区?

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

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

4.2为什么要有段?

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

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

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

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

4.3为什么要有碎片区?

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

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

所以此后为某个段分配存储空间的策略是这样的:

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

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

4.4区的分类

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

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

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

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

5.表空间

表空间可以看做是InnoDB存储弓|擎逻辑结构的最高层,所有的数据都存放在表空间中。

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

5.1独立表空间

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

空间可以回收(DROP TABLE操作可自动回收表空间;其他情况,表空间不能自己回收)。如果对于统计分析或是日志表,删除大量数据后可以通过: alter table TableName engine=innodb; 回收不用的空间。对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。

独立表空间结构

独立表空间由段、区、页组成。前面已经讲解过了。

真实表空间对应的文件大小
我们到数据目录里看,会发现-一个新建的表对应的.ibd文件只占用了96K,才6个页面大小(MySQL5.7中) ,这
是因为一开始表空间占用的空间很小,因为表里边都没有数据。不过别忘了这些.bd文件是自扩展的,随着表中
数据的增多,表空间对应的文件也逐渐增大。

查看InnoDB的表空间类型:

mysql > show variables like 'innodb_file_per_table';

image.png
你能看到innodb_file_per_table=ON, 这就意味着每张表都会单独保存为一个.ibd文件。

5.2系统表空间

系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间,在系统表空间中会
额外记录- -些有关整个系统信息的页面,这部分是独立表空间中段有的。

InnoDB数据字典

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

先要校验一下插入语句对应的表存不存在, 插入的列和表中的列是否符合,如果语法没有问题的话,还需要知道该表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面,然后把记录插入对应索引|的B+树中。所以说,MySQL除 了保存着我们插入的用户数据之外,还需要保存许多额外的信息,比方说:

某个表属于哪个表空间,表里边有多少列 表对应的每一一个列的类型是什么 该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面 该表有哪些外键,外键对应哪个表的哪些列 某个表空间对应文件系统上文件路径是什么

上述这些数据并不是我们使用INSERT语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得
已引入的一些额外数据,这些数据也称为元数据。InnoDB存储引擎特意定义了一些列的内部系统表(internal
system table)来记录这些这些元数据:
image.png
这些系统表也被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中SYS_TABLES
SYS_COLUMNSSYS_INDEXESSYS_FIELDS 这四个表尤其重要,称之为基本系统表(basic system tables) ,
我们先看看这4个表的结构:

SYS_TABLES表结构
image.png

SYS_COLUMNS表结构
image.png

SYS_INDEXES表结构
image.png
SYS_FIELDS表结构
image.png
注意:用户是不能直接访问InnoDB的这些内部系统表,除非你直接去解析系统表空间对应文件系统上的文件。不
过考虑到查看这些表的内容可能有助于大家分析问题,所以在系统数据库information_schema中提供了一些
innodb_sys 开头的表:

information_schema数据库中的这些以INNODB_SYS开头的表并不是真正的内部系统表(内部系统表就是我
们上边以SYS开头的那些表), 而是在存储弓|擎启动时读取这些以SYS开头的系统表,然后填充到这些以INNODB_SYS 开头的表中。以INNODB_SYS 开头的表和以SYS开头的表中的字段并不完全一样,但供大家参考已
经足矣。

附录:数据页加载的三种方式

InnoDB从磁盘中读取数据的最小单位是数据页。而你想得到的id = xx的数据,就是这个数据页众多行中的一行。

对于MySQL存放的数据,逻辑概念上我们称之为表,在磁盘等物理层面而言是按数据页形式进行存放的,当其加
载到MySQL中我们称之为缓存页

如果缓冲池中没有该页数据,那么缓冲池有以下三种读取数据的方式,每种方式的读取效率都是不同的:

1.内存读取

如果该数据存在于内存中,基本上执行时间在1ms左右,效率还是很高的。
image.png

2. 随机读取

如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在10ms 左右,这10ms中有6ms是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有3ms是对可能发生的排队时间的估计值,另外还有1ms的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。
image.png

3. 顺序读取

顺序读取其实是一种批量读取的方式, 因为我们请求的数据在磁盘上往往都是相邻存储的,顺序读取可以帮我们批量读取页面,这样的话,一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘1/0操作了。如果一个磁盘
的吞吐量是40MB/S,那么对于一个16KB大小的页来说,一次可以顺序读取2560 (40MB/16KB) 个页,相当于一
个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个
页的效率要高。