Mysql详解——InnoDB的数据存储结构

更多关于数据库的知识可点击我的主页哦

一、InnoDB数据存储结构

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

索引结构给我们提供了高效的查询速度,索引信息以及数据记录都是存储在文件中的,确切来说是存储在页结构中。

不同存储引擎中数据的存放格式一般是不同的,由于InnoDB是MySQL的默认存储引擎,所以接下来来剖析InnoDB存储引擎的数据存储结构。

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

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

以页作为磁盘和内存之间数据交互的基本单位,也就是说一次最少从磁盘中读取16KB的数据到内存中,一次最少将内存中16KB的数据刷新到磁盘中。换句话说,数据库管理存储空间的基本单位是页,数据库IO操作的基本单位是页。

记录是按行来存储的,而读取是以页为单位进行的。

不同页之间通过双向链表相连,数据页内的记录按照主键值从小到大的顺序组成一个单链表,每个数据页都会为它内部的数据生成一个页目录,在查找时可以通过二分查找提高效率。

不同的数据库管理系统的页大小不同,在MySQL的InnoDB引擎中,默认页的大小是16KB,可以通过命令查看

show variables like '%innodb_page_size%'

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pCZIfRTC-1651029427367)(D:\File\笔记\成仙之路\Mysql\Mysql高级\assets\image-20220329213244301.png)]

1.2 页的上层结构

在数据库中,还存在(Extent),(Segment),和表空间(TableSpace)的概念。行,页,区段,表空间的关系如下图所示:

:是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。由于一个页为16KB,因此一个区的大小为16*64 = 1MB。

:由一个或多个区组成,区在文件系统中是一个连续分配的空间,不过段中区和区之间不要求是相邻的。段是数据库中的分配单位,当我们创建数据库表,索引时,就会创建相应的段,比如创建一张表时就会创建一个表段,创建一个索引时就会创建一个索引段。

表空间:是一个逻辑容器,一个表空间可以有一个或多个段。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间,用户表空间,撤销表空间,临时表空间等。

2. 页的内部结构

常见的页的类型有:数据页(保存B+树节点),系统页Undo页事务数据页等。数据页是我们最常用的页。

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

页结构示意图如下:

这7个部分的作用简单概括如下:

2.1 文件头和文件尾

(1)文件头:

大小:38个字节

作用:描述各种页的通用信息,例如页的编号,页的类型, 其上一页,下一页是谁等

构成:

  • FIL_PAGE_OFFSET(4字节):页号,每个页有唯一的页号。
  • FIL_PAGE_TYPE(2字节):表示该页的类型,例如数据页,Undo页等
  • FIL_PAGE_PREV(4字节)和FIL_PAGE_NEXT(4字节):分别代表本页的上一个和下一个页的页号,这样就通过建立一个双向链表把不同页串联起来。
  • FIL_PAGE_SPACE_OR_CHKSUM(4字节):页的校验和,和文件尾的校验和是一样的,成对存在。
  • FIL_PAGE_LSN(8字节):页面被最后修改时对应的日志序列位置

校验和的作用:

我们都知道,内存和磁盘数据交换的基本单位是页,当内存中的页被修改后,存储引擎会在某个时间将数据同步到磁盘中,但是有可能在同步过程中出现问题(例如断电),造成了该页传输不完整。

因此每当一个页在内存中被修改了,在同步之前就会计算出它的校验和,进行同步时,由于文件头在页面的前面,所以校验和会先被同步到磁盘中,当完全写完后,校验和也会被写到页的尾部。如果完全同步成功,那么文件头和文件尾的校验和应该是一致的,如果中间出现问题导致没有完全写完,那么文件头中的校验和代表着修改过的页,文件尾中的校验和代表着原先的页,二者不同意味着同步时发生了错误。

(2)文件尾:

大小:8个字节

构成:

  • 前4个字节代表页的校验和,和文件头的校验和是一样的,成对存在。
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN),和文件头的一样,也是为了检验页的完整性。

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

空闲空间就是还没用到的空间。

用户记录也就是我们的一条一条的数据,相互之间通过单链表连接。

那么具体用户记录中每一行是怎么存储的呢?行与行之间是怎么通过单链表连接的呢?这就需要了解一下行格式了。

行格式的记录头信息:

例如有page_demo这张表,使用的是Compact行格式:

mysql> CREATE TABLE page_demo(
    ->     c1 INT,    
    ->     c2 INT,
    ->     c3 VARCHAR(10000),
    ->     PRIMARY KEY (c1)
    -> ) CHARSET=ascii ROW_FORMAT=Compact;

这张表中记录的行格式示意图如下:

这里我们先把重心放在记录头信息这部分,其余的在后续章节中介绍。

记录头信息的各个属性如下:

将预留位去掉,简化后的行格式示意图如下:

此时插入4条记录:

INSERT INTO page_demo 
VALUES
(1, 100, 'song'), 
(2, 200, 'tong'), 
(3, 300, 'zhan'), 
(4, 400, 'lisi');

添加后如下:

1. delete_mask:这个属性标记着当前记录是否被删除,占用1个二进制位。

  • 为0表示记录没有被删除
  • 为1表示记录被删除了

原因:在磁盘中,记录之间是连续存储的,这些被删除的记录之所以不立即从磁盘中删除,是因为移除它们之后其他的记录需要在磁盘中重新排列,造成性能消耗。所以只是打一个删除标记,所有被删除的记录会通过next_record指针组成一个删除的垃圾链表,被这个链表占用的空间称为可重用空间,之后如果有新的记录插入到表中,可能会把这些空间覆盖掉。

2. min_rec_mask:B+树的每层非叶子结点的最小记录都会被设置该标记为1。

3. record_type:表示该记录的类型

一共有四种类型:

  • 0:表示普通记录
  • 1:表示B+树非叶子节点记录(目录项记录)
  • 2:表示最大记录
  • 3:表示最小记录

4. heap_no:表示当前记录在本页中的位置

​ 这里有个小知识点:我们自己添加的数据的heap_no会从2开始计数。MySQL会自动给每页添加两个记录,由于这两个记录并不是我们自己插入的,因此有时候也称为伪记录或者虚拟记录。这两个伪记录一个表示最小记录,一个表示最大记录,他们的heap_no值分别是0和1,代表着页内记录的开始和结束。

**5. n_owned:**页中的每一组的最后一条记录的n_owned字段会记录该组的记录数。

6. next_record:表示从当前记录到下一条记录的地址偏移量

最大最小记录:

InnoDB规定的最小记录和最大记录这两条记录的构造很简单,都是由5个字节的记录头信息和8字节的固定的部分组成,如图:

这两条记录不是我们自定义的,因此并不放在User Records那里,而是被单独放在一个称为Infimum + Supermum的部分,如图:

2.3 页目录、页头

(1)页目录:

在页中,记录是以单链表的形式连接在一起的,每次查找时都需要逐一遍历,效率非常差,因此在页结构中设计了页目录这一模块,也就是给页中的数据设计一个目录,通过二分法能快速查找。

页目录并不是将每页的所有索引列都放进数组中,因为对于非聚簇索引来说,本身每一页存储就是索引列+主键值,如果还将所有索引列放进数组中,那相当于又存放了两份重复的数据,这显然是很浪费空间的。

因此,页目录采取的方法是:将所有的记录(包括最小记录和最大记录,不包括被标记删除的记录)**分为几组,页目录存储每组最后一条记录的地址偏移量,这些地址偏移量按照先后顺序进行存储,**每组的地址偏移量也被称为槽(slot)。

在页目录中,第一组记录默认只有最小记录,也就是只有一条记录;最后一组的记录数量在1-8条之间,其余组的记录数量在4-8之间,这样做的好处是除了第一组以外,其余组的记录数尽可能平分。

**在每一组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段。**例如第一组中只有最小记录这一条记录,同时最小记录也是这一组中的最后一条记录,因此最小记录的n_owned字段为1。

如何在一个页中查找指定数据:

  • 首先通过二分法确定所在的分组(槽)

  • 通过记录的next_record遍历该分组中的记录

不同角度的图示:

(2)页头:

存储页中关于记录的状态信息,例如本页已经存储了多少条记录,第一条记录的地址是什么,页目录存储了多少个槽等。

3. 行格式

InnoDB存储引擎提供了4种不同类型的行格式:Compact,Redundant,Dynamic,Compressed

查看默认的行格式:select @@innodb_default_row_format,可以看到默认是 dynamic 行格式。

在创建表时可以指定行格式。

3.1 Compact

一条完整的记录其实可以被分为记录的额外信息和记录的真实信息两部分。

变长字段长度列表

MySQL支持一些变长的数据类型,例如VARCHAR,TEXT,BLOB等,这些数据类型修饰的字段称为变长字段。由于变长字段中存储多少个字节不是固定的,所以我们在存储数据时需要将这些数据占用的字节数也存储下来。

在Compact行格式中,把所有变长字段占用的字节长度都存放在记录的头部,从而形成一个变长字段长度列表。需要注意的是:这里存储的长度的顺序和字段顺序是反过来的,例如两个varchar字段在表中的顺序是a(10),b(15),那么在变长字段长度列表中长度的顺序是15,10,是反过来的。

mysql> CREATE TABLE record_test_table (    
    ->     col1 VARCHAR(8),    
    ->     col2 VARCHAR(8) NOT NULL,    
    ->     col3 CHAR(8),    
    ->     col4 VARCHAR(8)    
    -> ) CHARSET=ascii ROW_FORMAT=COMPACT;

向表中插入两条记录:

INSERT INTO record_test_table(col1, col2, col3, col4) 
VALUES
('zhangsan', 'lisi', 'wangwu', 'songhk'), 
('tong', 'chen', NULL, NULL);       

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eVyCgqKH-1651029427370)(D:\File\笔记\成仙之路\Mysql\Mysql高级\assets\image-20220330155047260.png)]

NULL值列表

Compact行格式会将可以为NULL的列统一管理起来,存在一个标记为NULL值的列表中。如果表中没有允许为NULL的列,那么NULL值列表也就不存在了。

  • 当二进制位的值为1时,表示该列的值为NULL
  • 当二进制位的值为0时,表示该列的值不为NULL
  • 二进制的顺序和字段的顺序也是相反的。

举例:有字段a,b,c,其中a是主键,在某一行中存储的数依次为:a=1,b=null,c=2。那么Compact行格式中的NULL值列表会存储01。第一个0表示c不为NULL,第二个1表示b为NULL,由于a是主键,因此肯定非空,于是就会跳过。

记录头信息:

上面讲解页的内部结构中已经有详细介绍了

记录的真实数据:

记录的真实数据除了我们自定义的列数据外,还有三个隐藏列:

列名 是否必须 占用空间 描述
DB_ROW_ID 6字节 行ID,唯一标识一条记录
DB_TRX_ID 6字节 事务ID
DB_ROLL_PTR 7字节 回滚指针

DB_ROW_ID:一个表如果没有手动定义主键,InnoDB会自动选择一个Unique字段作为主键,如果没有Unique修饰的字段,则会自动为表添加一个DB_ROW_ID的隐藏列作为主键。

事务ID 和 回滚指针在后续事务和日志中进行讲解。

3.2 Dynamic和Compressed

行溢出:

一般认为,MySQL中的varchar类型可以存放65535个字节。下面我们可以来测试以下:

CREATE  TABLE  varchar_size_demo( 
    c  VARCHAR(65535) 
)  CHARSET=ascii  ROW_FORMAT=Compact;

结果报错了:Row size too large.

MySQL对一条记录占用的最大存储空间是有限制的,除BLOB或者TEXT类型的列之外, 其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。

这个65535个字节除了列本身的数据之外,还包括一些其他的数据,以Compact行格式为例,比如说我们为了存储一个VARCHAR(M)类型的列,除了真实数据占有空间以外,还需要记录的额外信息:2个字节的长度和1个字节的NULL值标识(可无)。因此,如果这个字段有NOT NULL属性,那么它真正的最大长度应该是65535 - 2(长度) = 65533个字节。

我们可以知道一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出。

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

Compressed 和 Dynamic 两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。

Compressed行记录格式的另一个功能就是,**存储在其中的行数据会以zlib的算法进行压缩,**因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。

3.3 Redundant

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

Redundant和Compact的区别主要在于字段长度偏移列表和记录头信息这两部分。

Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:

  • 少了“变长”两个字:Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。

  • 多了“偏移”两个字:这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度

另外,Redundant行格式中的记录头信息固定占用6个字节(48位),而Compact是5个字节。

与Compact行格式的记录头信息对比来看,有两处不同:

  • Redundant行格式多了n_field和1byte_offs_flag这两个属性。

  • Redundant行格式没有record_type这个属性。

这里就不详细了解了,由于Redundant本身也是老的格式了,所以有兴趣可以自行上网查找资料。

4. 区、段和碎片区

4.1 为什么要有区:

前面我们讲到磁盘和内存的数据交换的单位是页。而如果以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能会相距非常远。这样当我们进行范围查找时,由于页跟页之间有可能相差很多,导致磁盘效率很低,因此我们应该尽量让链表中相邻的页的位置也尽可能相邻,于是引入区的概念。

一个区就是在物理位置上连续的64个页因为InnoDB默认的页为16KB,所以一个区的大小为16*64 = 1MB。当表的数据量特别大时,为某个索引分配空间时就不再按照页为单位进行分配了,而是按照区为单位分配,甚至表的数据特别多时,可以一次性分配多个连续的区,虽然可能会造成一点空间浪费(数据不足无法填满整个区),但是从性能来看,功大于过!

4.2 为什么要有段:

对于范围查找,其实是对叶子节点的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请的区中的话,进行范围查找时效果就大打折扣了。

所以InnoDB对叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己的区,非叶子节点有自己的区。存放叶子节点的集合为一个段,存放非叶子节点的集合为另一个段,也就是说一个索引会有两个段。

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

4.3 为什么要有碎片区:

默认情况下,一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成两个段,而段是以区为单位申请存储空间的,一个区默认为1M,所以默认情况下一个即使只有几条记录的小表也需要2M的存储空间,并且每次添加一个索引就需要2M,这显然是很浪费的。

为了解决这个问题,InnoDB提供了碎片区的概念。**在一个碎片区中,并不是所有的页都是存储同一个段的数据,而是碎片区中的页可以用于不同的段,**比如有些页用于段A,有些页用于段B,有些页甚至不属于任何一个段。碎片区直属于表空间,并不属于某一个段。

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

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

所以段并不能简单定义成某些区的集合,而应该是某些零散页面以及一些完整的区的集合。

5. 表空间

表空间可以看作数据存储的最高层, 所有的数据都存放在表空间中。

表空间存储的对象是段,一个表空间可以有多个段。

表空间从管理上可以划分为系统表空间,独立表空间,撤销表空间,临时表空间等

5.1 独立表空间

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

查看InnoDB的表空间类型:

show variables like 'innodb_file_per_table';

5.2 系统表空间

猜你喜欢

转载自blog.csdn.net/OYMNCHR/article/details/124446658