第七章InnoDB数据存储结构
1. 数据的存储结构:页
索引结构为我们提供了高效的索引方式,不过索引信息和数据记录都是保存在文件上的,确切的来说是存储在页结构中,另一方面,索引是在存储引擎中实现的,Mysql服务器上的存储引擎负责对表中数据的读取和写入操作
不同存储引擎存放的格式一般是不同的,甚至所有的存储引擎比如Memory都不用磁盘来存储数据
由于InnoDB是Mysql的默认存储引擎
1.1磁盘与内存交互基本单位:页
InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB。
以页作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。也就是说,**在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页(Page),数据库Ⅳ/o操作的最小单位是页。**一个页中可以存储多个行记录,
记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次I/o操作)只能处理一行数据,效率会非常低。
1.2页结构概述
页a、页b、页c…页n这些页可以不在物理结构上相连,只要通过双向链表相关联即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
1.3页的大小
不同的数据库管理系统(简称DBMS )的页大小不同。比如在MySQL的InnoDB存储引擎中,默认页的大小是16KB,我们可以通过下面的命令来进行查看:
mysql> show variables like '%innodb_page_size%' ;
SQL Server中页的大小为8KB,而在oracle 中我们用术语“块”(Block)来代表"页”,Oralce支持的块大小为2KB,4KB,8KB,16KB,32KB和64KB。
1.4 页的上层结构
另外在数据库中还存在着区(Extent),段(Segment)和表空间(Tablespace)的概念,行,页,区,段,表空间的关系如下图所示
区(Extent))是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64 个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB。
段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于多个表空间,数据库是由一个或者多个表空间组成,表空间从管理上可以划分为系统表空间,用户表空间,撤销表空间,临时表空间
2. 页的内部结构
页如果按类型划分的话,常见的有数据页(保存B+树节点),系统页,Undo页和事务数据页等,数据页是我们最常使用的页.
数据页的16KB大小的存储空间被划分为七个部分,分别为文件头(File Header),页头(Page Header),最大最小记录(Infimum+supremum),用户记录(User
Records),空闲空间(Free Space),页目录(Page Directory)和文件尾(Free Tailer)
页结构示意图
这七个部分作用分别如下
第一部分:File Header(文件头部) 和FIle Traier(文件尾部)
首先是文件通用部分 ,也就是文件头和文件尾
文件头部信息
直接删除的话链表需要重新排列,消耗时间,所以只是把那个位的值改为1 即可
2.3 从数据页的角度看B+树如何查询
一颗B+树按照节点类型分为两部分:
- 叶子节点,b+树最底层的节点,节点高度为0,存储行记录
- 非叶子结点,结点高度大于0,存储索引键和页面指针,并不存储记录本身
B+树是如何进行记录检索的?
如果
3.行格式
像之前的聚簇索引通常拿主键默认创建,如果没有主键就从表中选一个unique声明的字段作为索引,如果还是没有就系统设置一个隐藏的主键
行溢出
varchar 65532+2个字节的变长字段长度+1个null值的标识
通过上面的案例,我们可以知道一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出。
在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中进行分页存储,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。这称为页的扩展,举例如下:
在MySQL 8.0中,默认行格式就是Dynamic,Dynamic、Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有分歧:
- Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢
出的方式。如图,在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。 - Compact和Redundant两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)。
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储
Redundant行格式
Redundant是MySQL 5.0版本之前InnoDB的行记录存储方式,MySQL 5.0支持Redundant是为了兼容之前版本的页格式。
现在我们把表record_test_table的行格式修改为Redundant:ALTER TABLE record_test_table RowlL_FORMAT=Redundant;Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: o Warnings: o
从上图可以看到,不同与Compact行格式记录,Redundant行格式的首部是一个字段长度偏移量,同样是按照列的顺序逆序放置的
Compact是记录变长的部分,Redundant是都记录,所以是冗余的
注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:
- 少了“变长”两个字:Redundant行格式会把该条记录中所有列(包括隐藏列)的
长度信息都按照逆序存储到字段长度偏移列表。 - 多了“偏移”两个字:这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
举例:比如第一条记录的字段长度偏移列表就是:
2B25 1F 1B 13 oC 06
因为它是逆序排放的,所以按照列的顺序排列就是:06 0C 13 17 1A 24 25
按照两个相邻数值的差值来计算各个列值的长度的意思就是:
第一列(row_id)的长度就是Ox06个字节,也就是6个字节。
第二列(transaction_id)的长度就是(OxOC- 0x06)个字节,也就是6个字节。第三列(roll_pointer)的长度就是(Ox13 - oxOC)个字节,也就是7个字节。第四列(col1)的长度就是(Ox1B - 0x13)个字节,也就是8个字节。
第五列(col2)的长度就是(Ox1F - Ox1B)个字节,也就是4个字节。
4. 区,段与碎片区
4.1 为什么要有区?
B+树的每一层中的页都会形成一个双向链表,如果以页为单位分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得特别远我们介绍B+索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中的相邻的两个页物理离得非常远,就是所谓的随机I/O.磁盘的速度和内存相差好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以进行所谓的顺序I/O
引入区的概念,一个区就是在物理位置上连续的64个页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB= 1MB。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足以填充满整个区),但是从性能角度看,可以消除很多的随机i/o,功大于过!
4.2 为什么要有段
对于范围查询,其实对b+树的树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放入区里面的话,范围查找的效果就会大打折扣了,所以Innodb对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也是,存放叶子节点的区的集合就算是一个段,存放非叶子节点的区的集合也是一个段,也就一个索引会生成两个段,一个叶子节点段一个非叶子节点段
除了索引的叶子节点段和非叶子节点段之外,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):每一个索引都可以分为叶子节点段和非叶子节点段。
处于FREE、FREE_FRAG以及FULL_FRAG这三种状态的区都是独立的,直属于表空间。而处于FSEG状态的区是附属于某个段的。
如果把表空间比作是一个集团军,段就相当于师,区就相当于团。一般的团都是隶属于某个师的,就像是处于FSEG的区全都隶属于某个段,而处于FREE、FREE_FRAG以及FULL_FRAG这三种状态的区却直接隶属于表空间,就像独立团直接听命于军部一样。
5.表空间
表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。
表空间是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。表空间数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间(System
tablespace)、独立表空间(File-per-table tablespace)、撤销表空间(Undo Tablespace)和临时表空间(Temporary Tablespace)等。
5.1独立表空间
独立表空间,即每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立的表空间(即:单表)可以在不同的数据库之间进行迁移。
空间可以回收(DROPTABLE操作可自动回收表空间;其他情况,表空间不能自己回收)。如果对于统计分析或是日志表,删除大量数据后可以通过: alter table TableName engine=innodb;
回收不用的空间。对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。
独立表空间结构
独立表空间由段、区、页组成。前面已经讲解过了。
真实表空间对应的文件大小
我们到数据目录里看,会发现一个新建的表对应的.ibd文件只占用了96K,才6个页面大小(MysQL5.7中),这是因为一开始表空间占用的空间很小,因为表里边都没有数据。不过别忘了这些.ibd文件是自扩展的,随着表中数据的增多,表空间对应的文件也逐渐增大。
查看Innodb的表空间类型
mysql > show variables like 'innodb_file_per_table';
你能看到innodb_ file_ per, _table=ON, 这就意味着每张表都会单独保存为一个.ibd文件。
5.2系统表空间
系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间, 在系统表空间中会
额外记录-一些有关整个系统信息的页面,这部分是独立表空间中没有的。
InnoDB数据字典
每当我们向一个表中插入-条记录的时候,MySQL校验过程 如下:
先要校验一下插入语句对应的表存不存在, 插入的列和表中的列是否符合,如果语法没有问题的话,还需要知道
该表的聚簇索引和所有二级索弓|对应的根页面是哪个表空间的哪个页面,然后把记录插入对应索引的B+树中。所
以说,MySQL除 了保存着我们插入的用户数据之外,还需要保存许多额外的信息,比方说:
某个表属于哪个表空间,表里边有多少列
表对应的每一一个列的 类型是什么
该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面
该表有哪些外键,外键对应哪个表的哪些列
某个表空间对应文件系统上文件路径是什么
上述这些数据并不是我们使用INSERT语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得
已引入的一-些额外数据,这些数据也称为元数据。InnoDB存储引擎特意定义了一些列的内部系统表(internal
system table)来记录这些这些元数据:
这些系统表也被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中SYS_TABLES、
SYS_COLUMNS、 SYS_INDEXES、 SYS_FIELDS 这四个表尤其重要,称之为基本系统表(basic system tables),
我们先看看这4个表的结构:
注意:用户是不能直接访问InnoDB的这些内部系统表,除非你直接去解析系统表空间对应文件系统上的文件。不
过考虑到查看这些表的内容可能有助于大家分析问题,所以在系统数据库information_ schema中提供了一-些
以innodb_ sys开头的表:
mysql> USE info rmation_ schema ;
Database changed
mysql> SHOW TABLES LIKE‘innodb_ sys%' ;
在information_ schema数据库中的这些以INNODB_ SYS 开头的表并不是真正的内部系统表(内部系统表就是我
们上边以SYS开头的那些表),而是在存储引擎启动时读取这些以SYS开头的系统表,然后填充到这些以
INNODB_ SYS 开头的表中。以INNODB SYS 开头的表和以SYS开头的表中的字段并不完全-样,但供大家参考已
经足矣。
附录:数据页加载的三种方式
InnoDB从磁盘中读取数据的最小单位是数据页。而你想得到的id =xx的数据,就是这个数据页众多行中的一行。对于MySQL存放的数据,逻辑概念上我们称之为表,在磁盘等物理层面而言是按数据页形式进行存放的,当其加载到MySQL中我们称之为缓存页。
如果缓冲池中没有该页数据,那么缓冲池有以下三种读取数据的方式,每种方式的读取效率都是不同的:
1. 内存读取
如果该数据存在于内存中,基本上执行时间在1ms左右,效率还是很高的。
2.随机读取
如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在10ms 左右,这10ms 中有6ms是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有3ms是对可能发生的排队时间的估计值,另外还有1ms的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。
3.顺序读取
[外链图片转存中…(img-coPXd3cf-1694608182047)]
2.随机读取
如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在10ms 左右,这10ms 中有6ms是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有3ms是对可能发生的排队时间的估计值,另外还有1ms的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。
[外链图片转存中…(img-zEjDShiR-1694608182048)]
3.顺序读取
顺序读取其实是一种批量读取的方式,因为我们请求的数据在磁盘上往往都是相邻存储的,顺序读取可以帮我们批量读取页面,这样的话,一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘I/o操作了。如果一个磁盘的吞吐量是40MB/9, 那么对于一个16KB大小的页来说,一次可以顺序读取2560 (40MB/16KB)个页,相当于-个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。