[https://www.bilibili.com/video/BV1iq4y1u7vj]
个人根据 尚硅谷宋红康Mysql高级视频总结的学习笔记
索引结构给我们提供了高效的索引方式,不过索引信息以及数据记录都是保存在文件上的,确切说是存储在页结构中。另一方面,索引是在存储引擎中实现的,MSQL服务器上的存储引擎负责对表中数据的读取和写入工作。不同存储引擎中存放的格式一般是不同的,甚至有的存储引擎比如Memory(内存)都不用磁盘来存储数据。
InnoDB是MySQL的默认存储引擎。
磁盘内存交互基本单位:页
InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB。
以页作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中16KB内容刷新到磁盘中。也就是说,在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页,数据库I/O操作的最小单位是页。一个页中可以存储多个行记录。
记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。
页结构概述
页a、页b、页c…页n 这些页可以不在物理结构上相连,只要通过双向链表相关联即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
mysql > show variables like '%innodb_page_size%'
页的上层结构
在数据库中,还存在着区(Extent)、段(Segment)和表空间(Tablespace)的概念。行、页、区、段、表空间的关系如下图所示:
区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64 * 16 KB = 1MB
段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段时数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。
页的内部结构
页如果按照类型划分的话,常见的有数据页(保存 B+树节点)、系统页、Undo页 和事务数据页 等。数据页是我们最常使用的页。
数据页的16KB大小的存储空间被划分为七个部分,如下图所示:
B+树是如何进行记录检索的?
如果通过B+树的索引查询行记录,首先是从B+树的根开始,逐层检索,直到找到叶子结点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录的槽采用二分查找的方式先找到一个粗略的记录分组,然后再在分组中通过链表遍历的方式查找记录。
普通索引和唯一索引在查询效率上有什么不同?
唯一索引就是在普通索引上增加了约束性,也就是关键字唯一,找到了关键字就停止索引。而普通索引,可能会存在用户记录中的关键字相同的情况,根据页结构的原理,当我们读取一条记录的时候,不是单独将这条记录从磁盘中度出去,而是将这个记录所在的页加载到内存中进行读取。InnoDB存储引擎的页大小为16KB,在一个页中可能存储着上千个记录,因此在普通索引的字段上进行查找也就是在内存中多几次“判断下一条记录”的操作,对于CPU来说,这些操作所消耗的时间是可以忽略不计的。所以对一个索引字段进行检索,采用普通索引还是唯一索引在检索效率上基本上没有差别。
区、段、碎片区
为什么要有区?
B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。B+树索引在范围查询的时候只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的所及I/O。磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O。
引入区的概念,一个区就是在物理位置上连续的64个页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64 * 16KB = 1MB。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配,深知在表中的数据特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足以填充满整个区),但是从性能角度看,可以消除很多的随机I/O。
加载页需要存储空间,我们不能保证所有的页在物理磁盘上都是相邻存放的,但是我们可以保证一波数据页是连续的,另一波数据页是连续的。
为什么要有段?
对于范围查询,其实是对B+树叶子结点中的记录进行顺序扫描,而如果不区分叶子结点和非叶子结点,统统把结点代表的页面放到申请到的区中的话,进行范围扫描的效果就大大折扣了。所以InnoDB对B+树的叶子结点和非叶子结点进行了区别对待,也就是说叶子结点有自己独有的区,非叶子结点也有自己独有的区。存放叶子结点的区的结合就算是一个段(segment)存放非叶子结点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子结点段,一个非叶子结点段。
除了索引的叶子结点段和非叶子结点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段。所以,常见的段有数据段、索引段、回滚段。数据段即为B+树的叶子结点,索引段即为B+树的非叶子结点。
在InnoDB存储引擎中,对段的管理都是由引擎自身所完成的,DBA不能也没有必要对其进行控制。
段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。
为什么要有碎片区?
默认情况下,提个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M(64 * 16KB =1024KB)存储空间,所以默认情况下一个只存了几条记录的小表也需要2M的存储空间么?以后每次添加一个索引都要多申请2M的存储空间么?这对于一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用。
为了考虑以完整的区为单位分配给某个段对于数量较小的表太浪费存储空间的这种情况,InnoDB提出了一个碎片区的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片去只属于表空间,并不属于任何一个段。
为某个段分配存储空间的策略是这样的:
-
在刚开始向中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
-
当某个段已经占用了32个碎片区页面之后,就会申请以完整的区为单位来分配存储空间。
所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面以及一些完整的区的集合。
区的分类
区大体上可以分为4种类型:
- 空闲的区(FREE):现在还没有用到这个区中的任何页面。
- 有剩余空间的碎片区(FREE_FRAG):表示碎片区中还有可用的页面。
- 没有剩余空间的碎片区(FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面。
- 附属于某个段的区(FSEG):每一个索引都可以分为叶子结点段和非叶子结点段。
处于FREE、FREE_FRAG以及FULL——FRAG这三种状态的区都是独立的,直属于表空间。而处于FSEG状态的区是附属于某个段的。
表空间
表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。表空间是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。表空间数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、独立表空间、撤销表空间和临时表空间等。
独立表空间
独立表空间,即每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立的表空间(即:单表)可以在不同的数据库之间进行迁移。
空间回收(DROP TABLE操作可自回收表空间;其他情况,表空间不能自己回收)。对于统计分析或日志表,删除大量数据后可以通过:alter table TableName engine = innodb;回收不用的空间。对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。
独立表空间结构
独立表空间由段、区、页组成。
真实表空间对应的文件大小
我们到数据目录里看,会发现一个新建的表对应的.ibd文件只占用了96K,才6个页面大小(mysql 5.7中),这是因为一开始表空间占用的空间很小,因为表里边都没有数据。不过别忘了这些.ibd文件是自扩展的,随着表数据的增多,表空间对应的文件也逐渐增大。在mysql8.0中,.frm文件与.ibd文件合一,因此.ibd文件有7个页面大小。
查看InnoDB的表空间类型
mysql > show variables like 'innodb_file_per_table';
innodb_file_per_table= ON,这就意味着每张表都会单独保存一个.ibd文件。
系统表空间
系统表空间的结构和独立表空间基本类似,只不过由于整个Mysql进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,这部分是独立表空间中没有的。
InnoDB数据字典
每当我们向一个表中插入一条记录的时候,MySQL校验过程如下:
先要校验一下插入语句对应的表存不存在,插入的列和表中的列是否符合,如果语法没有问题的话,还需要知道该表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面,然后把记录插入对应索引的B+树中。所以说,MySQL除了保存着我们插入的用户数据之外,还需要保存许多额外的信息,如:
- 某个表属于哪个表空间,表里边有多少列
- 表对应的每一个列的类型是什么
- 该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面
- 该表有哪些外键,外键对应哪个表的哪些列
- 某个表空间对应文件系统上文件路径是什么
上述这些数据并不是我们使用Insert语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得已引入的一些额外数据,这些数据也称为元数据。InnoDB存储引擎特意定义了一些列的内部系统表来记录这些元数据:
这些系统表也被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面,其中SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四个表尤其重要,称之为基本系统表。
注意:用户是不能直接访问InnoDB的这些内部系统表,除非你直接去解析系统表空间对应文件系统上的文件。
系统数据库information_schema中提供了一些以innodb_sys开头的表,可以查询这些表的内容。
在information_schema数据库中的这些以INNODB_SYS开头的表并不是真正的内部系统表(内部系统表就是我们上边以SYS开头的那些表),而是在存储引擎启动时读取这些以SYS开头的系统表,然后填充到这些以INNODB_SYS开头的表中。以INNODB_SYS开头的表和以SYS开头的表中的字段并不完全一样。
数据页加载的三种方式: