在前面的两篇文章已经对InnoDB索引的结构,页存储结构,行格式做了十分细致的分析,也详细阐述了为什么你的SQL会慢,索引命中的原理,接下来我要继续深入学习MySQL。在此之前还要先来补充一下MySQL的一些基础知识。

一,MySQL的数据目录

1. 数据库和文件系统的关系

InnoDB,MyISAM这样的存储引擎都是把表存储在磁盘上,而操作系统是使用文件系统来管理磁盘的。【像InnoDB,MyISAM这样的存储引擎都是把数据存储在文件系统上的。】当我们想读取数据的时候,这些存储引擎会从文件系统中把数据读出来返回给我们,当我们想写入数据的时候,这些存储引擎又会把数据写回到文件系统。

本小节主要就是分析下InnoDB,MyISAM两个存储引擎的数据是如何在文件系统中存储的。

我的MySQL版本是5.7.28,所以接下来的操作和分析都是基于这个小版本的。其他版本可能会有细微的差异。

2.MySQL数据目录

MySQL服务器程序在启动时,会到文件系统的某个目录下加载一些数据,之后再运行过程中产生的数据也会存储到这个目录下的某些文件中。这个目录就是数据目录。

2.1 数据目录和安装目录的区别

MySQL的安装目录是在安装MySQL的时候指定的安装位置,下面有个很重要的bin目录,里面存储着控制客户端程序与服务器程序的命令。

MySQL的数据目录是用来存储MySQL在运行过程中产生的数据。

2.2 MySQL的数据目录在哪里

数据目录对应着一个系统变量datadir,在使用客户端与服务器建立连接以后,查看这个系统变量的值就知道了:

  1. show variables like 'datadir';

结果如下:

  1. mysql> show variables like 'datadir';
  2. +---------------+--------------------+
  3. | Variable_name | Value |
  4. +---------------+--------------------+
  5. | datadir | C:\yhd\mysql\Data\ |
  6. +---------------+--------------------+
  7. 1 row in set, 1 warning (0.00 sec)

3.数据目录的结构

3.1 数据库在文件系统中的表示

每个数据库都对应着数据目录下的一个子目录,或者说对应着一个文件夹。当我们创建一个新的数据库的时候,MySQL会帮助我们做两件事:

  1. 在数据目录下创建一个与数据库同名的文件目录
  2. 在该子目录下创建一个db.opt文件。这个文件中包含了数据库的一些属性,比如该数据库的字符集和比较规则。

下面来看一下我的MySQL中的数据库:

  1. mysql> show databases;
  2. +--------------------+
  3. | Database |
  4. +--------------------+
  5. | information_schema |
  6. | mysql |
  7. | performance_schema |
  8. | sys |
  9. | yhd |
  10. +--------------------+
  11. 5 rows in set (0.00 sec)

再从数据目录里看一下:

  1. C:\yhd\mysql\Data>dir
  2. 驱动器 C 中的卷没有标签。
  3. 卷的序列号是 CA5F-90F5
  4. C:\yhd\mysql\Data 的目录
  5. 2021/12/19 00:27 <DIR> .
  6. 2021/12/19 00:27 <DIR> ..
  7. 2021/12/14 00:00 56 auto.cnf
  8. 2021/12/14 00:00 1,703 ca-key.pem
  9. 2021/12/14 00:00 1,131 ca.pem
  10. 2021/12/14 00:00 1,131 client-cert.pem
  11. 2021/12/14 00:00 1,707 client-key.pem
  12. 2021/12/14 00:12 696 DESKTOP-NJIMTJP-slow.log
  13. 2021/12/18 01:15 25,345 DESKTOP-NJIMTJP.err
  14. 2021/12/18 01:15 5 DESKTOP-NJIMTJP.pid
  15. 2021/12/19 02:18 79,691,776 ibdata1
  16. 2021/12/18 01:15 12,582,912 ibtmp1
  17. 2021/12/18 01:15 356 ib_buffer_pool
  18. 2021/12/19 02:18 50,331,648 ib_logfile0
  19. 2021/12/19 02:18 50,331,648 ib_logfile1
  20. 2021/12/14 00:00 <DIR> mysql
  21. 2021/12/14 00:00 <DIR> performance_schema
  22. 2021/12/14 00:00 1,707 private_key.pem
  23. 2021/12/14 00:00 461 public_key.pem
  24. 2021/12/14 00:00 1,131 server-cert.pem
  25. 2021/12/14 00:00 1,707 server-key.pem
  26. 2021/12/14 00:00 <DIR> sys
  27. 2021/12/19 00:56 <DIR> yhd
  28. 17 个文件 192,975,120 字节
  29. 6 个目录 189,156,126,720 可用字节
  30. C:\yhd\mysql\Data>

仔细看会发现,除了information_schema这个数据库以外,其他的数据库都对应一个文件目录,这个数据库有点特殊,后面在具体分析。

3.2 表在文件系统中的表示

我们的数据其实是以记录的形式插入到表中的。每个表的信息其实可以分为两种。

  1. 表结构信息
  2. 表数据信息

为了保存表结构信息,InnoDB,MyISAM这两种存储引擎都会在数据目录下对应的数据库子目录中创建一个专门用于描述表结构的文件,文件名是表名.frm。这个文件是二进制格式的,直接打开会乱码。

我们知道不同的存储引擎对于表中的数据存储是不一样的,接下来我们分别来看一下InnoDB,MyISAM是如何存储表中的数据的。

InnoDb是如何存储数据的

我们再来回顾下上一篇的知识:

  1. innoDB其实是使用页来作为基本单位管理存储空间的,默认大小16KB。
  2. 对于InnoDB存储引擎来说,每个索引都对应一颗B+树,该B+树的每个结点都是一个数据页。数据页之间没有必要是物理连续的,因为数据页之间有双向链表来维护这些页的顺序。
  3. InnoDB的聚簇索引的叶子结点存储了完整的用户记录,也就是所谓的索引即数据,数据即索引。

为了更好的管理这些页,InnoDB提出了表空间或者文件空间的概念。这个表空间是一个抽象的概念,他可以对应文件系统上一个或者多个真实文件(不同表空间对应的文件数量可能不同)。每一个表空间可以被划分为很多个页,表数据被存放在某个表空间下的某些页中。InnoDB将表空间划分几种不同的类型,我们一个个分析一下子。

  1. 系统表空间

这个系统表空间可以对应文件系统上一个或者多个实际的文件。在默认情况下,InnoDB会在数据目录下创建一个名为ibdata1,大小为12MB的文件,这个文件就是对应的系统表空间在文件系统上的表示。怎么才12MB?这是因为这个文件是自扩展文件,也就是当不够用的时候会自己增加文件大小。

当然,如果想让系统表空间对应文件系统上的多个实际文件,或者仅仅觉得原来的ibdata1这个文件名难听,那么可以在MySQL服务启动的时候,配置对应的文件路径以及他们的大小。比如像下面这样修改配置文件:

  1. [server]
  2. innodb_data_file_path=data1:512M;data2:512M:autoextend

这样,在MySQL启动之后会创建data1和data2这两个各自512MB大小的文件作为系统表空间。其中的autoextend表明,如果这两个文件不够用,则会自动扩展data2文件的大小。

我们也可以把系统表空间对应的文件路径不配置到数据目录下,甚至可以配置到单独的磁盘分区上,涉及到的启动参数就是innodb_data_file_pathinnodb_data_home_dir

需要注意的一点是,在一个MySQL服务器中,系统表空间只有一份。从MySQL5.5.7到MySQL5.6.6之间的各个版本中,我们表中的数据都会被默认存储到这个 系统表空间

ibdata1.png

  1. 独立表空间

在MySQL5.6.6以及以后的版本中,InnoDB不在默认把各个表的数据存储到系统表空间,而是为每一个表建立一个独立的表空间,也就是说,创建多少张表就会对应多少个表空间。在使用独立表空间来存储表数据的时候,会在该表所属的数据库对应的子目录下创建一个表示该独立表空间的文件,其文件名和表名相同,只不过添加了一个.ibd扩展名。所以完整的文件名称:表名.ibd

假如我们使用独立表空间来存储yhd数据库下的person_info表,那么在该数据库所对应的yhd文件目录下会为person_info表创建下面两个文件:person_info.frm,person_info.ibd

其中ibd文件用来存储表中的数据。当然也可以自己指定是使用系统表空间还是独立表空间来存储数据。

  1. 其他类型表空间

除了上述两种表空间之外,还有一些不同类型的表空间,比如通用表空间,undo表空间,临时表空间。

MyISAM是如何存储数据的

索引和数据在InnoDB是一回事,但是MyISAM中的索引相当于全部都是二级索引,该存储引擎的数据和索引是分开存放的。所以在文件系统中也是使用不同的文件来存储数据文件和索引文件,而且与InnoDB不同的是,MyISAM并没有什么表空间一说,表的数据和索引都存放到对应的数据库子目录下。

假设我们person_info表使用的是MyISAM存储引擎,那么在它所在数据库对应的yhd文件目录下会为person_info创建三个文件:person_info.frm,person_info.MYD,person_info.MYI

其中person_info.MYD表示表的数据文件,也就是插入的用户记录,person_info.MYI表示表的索引文件,我们为该表创建的索引都会放到这个文件中。

3.3 其他的文件

除了我们上边说的这些用户自己存储的数据以外,数据目录下还包括为了更好运行程序的一些额外文件,主要包括这几种类型的文件:

  • 服务器进程文件。
    我们知道每运行一个MySQL服务器程序,都意味着启动一个进程。MySQL服务器会把自己的进程ID写入到一个文件中。
  • 服务器日志文件。
    在服务器运行过程中,会产生各种各样的日志,比如常规的查询日志、错误日志、二进制日志、redo日志等各种日志,这些日志各有各的用途,现在先了解一下就可以了。
  • 默认/自动生成的SSL和RSA证书和密钥文件。
    主要是为了客户端和服务器安全通信而创建的一些文件

4.文件系统对数据库的影响

因为MySQL的数据都是存在文件系统中的,就不得不受到文件系统的一些制约,这在数据库和表的命名、表的大小和性能方面体现的比较明显,比如下边这些方面:

  • 数据库名称和表名称不得超过文件系统所允许的最大长度。
    每个数据库都对应数据目录的一个子目录,数据库名称就是这个子目录的名称;每个表都会在数据库子目录下产生一个和表名同名的.frm文件,如果是InnoDB的独立表空间或者使用MyISAM引擎还会有别的文件的名称与表名一致。这些目录或文件名的长度都受限于文件系统所允许的长度~
  • 特殊字符的问题
    为了避免因为数据库名和表名出现某些特殊字符而造成文件系统不支持的情况,MySQL会把数据库名和表名中所有除数字和拉丁字母以外的所有字符在文件名里都映射成 @+编码值的形式作为文件名。比方说我们创建的表的名称为'test?',由于?不属于数字或者拉丁字母,所以会被映射成编码值,所以这个表对应的.frm文件的名称就变成了test@003f.frm
  • 文件长度受文件系统最大长度限制
    对于InnoDB的独立表空间来说,每个表的数据都会被存储到一个与表名同名的.ibd文件中;对于MyISAM存储引擎来说,数据和索引会分别存放到与表同名的.MYD.MYI文件中。这些文件会随着表中记录的增加而增大,它们的大小受限于文件系统支持的最大文件大小。

5.MySQL系统数据库简介

我们前边提到了MySQL的几个系统数据库,这几个数据库包含了MySQL服务器运行过程中所需的一些信息以及一些运行状态信息,我们现在稍微了解一下。

  • mysql
    这个数据库贼核心,它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。
  • information_schema
    这个数据库保存着MySQL服务器维护的所有其他数据库的信息,比如有哪些表、哪些视图、哪些触发器、哪些列、哪些索引等等。这些信息并不是真实的用户数据,而是一些描述性信息,有时候也称之为元数据。
  • performance_schema
    这个数据库里主要保存MySQL服务器运行过程中的一些状态信息,算是对MySQL服务器的一个性能监控。包括统计最近执行了哪些语句,在执行过程的每个阶段都花费了多长时间,内存的使用情况等等信息。
  • sys
    这个数据库主要是通过视图的形式把information_schemaperformance_schema结合起来,让程序员可以更方便的了解MySQL服务器的一些性能信息。

二,回顾前面

数据页(也就是Index类型的页)由7部分组成,其中有两个部分是所有类型的页面都通用的。

所有类型的页都会包含下面两个部分。

  1. File Header:记录页面的一些通用信息
  2. File Trailer: 校验页是否完整,保证页面在从内存刷新到磁盘后内容是相同的

通用页结构示意图.png

名称 占用空间大小(字节) 描述
FIL_PAGE_SPACE_OR_CHKSUM 4 页的校验和(checksum值)
FIL_PAGE_OFFSET 4 页号
FIL_PAGE_PREV 4 上一个页的页号
FIL_PAGE_NEXT 4 下一个页的页号
FIL_PAGE_LSN 8 页面被最后修改时对应的日志序列位置
FIL_PAGE_TYPE 2 该页的类型
FIL_PAGE_FILE_FLUSH_LSN 8 仅仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 页属于哪个表空间
  • 表空间中的每一个页都对应着一个页号,也就是FIL_PAGE_OFFSET,这个页号由4个字节组成,也就是32个比特位,所以一个表空间最多可以拥有2³²个页,如果按照页的默认大小16KB来算,一个表空间最多支持64TB的数据。表空间的第一个页的页号为0,之后的页号分别是1,2,3…依此类推
  • 某些类型的页可以组成链表,链表中的页可以不按照物理顺序存储,而是根据FIL_PAGE_PREVFIL_PAGE_NEXT来存储上一个页和下一个页的页号。需要注意的是,这两个字段主要是为了INDEX类型的页,也就是我们之前一直说的数据页建立B+树后,为每层节点建立双向链表用的,一般类型的页是不使用这两个字段的。
  • 每个页的类型由FIL_PAGE_TYPE表示,比如像数据页的该字段的值就是0x45BF,不同类型的页在该字段上的值是不同的。

InnoDB支持许多种类型的表空间,我们暂时重点关注系统表空间和独立表空间的结构。他们结构比较相似,但是由于系统表空间中额外包含了一些关于整个系统的信息,所以我们先分析独立表空间,再说系统表空间。

三,独立表空间

1.区的概念

为了更好的管理表中的页,InnoDB提出了区的概念。对于16KB的页来说,连续的64个页就是一个区。也就是说一个区默认占用1M空间。不论是系统表空间还是独立表空间,都可以看成是由若干区组成的,每256个区划分为一个组

表空间结构.png

为什么要有区的概念?

从理论上来讲,不引入区的概念只使用页的概念对存储引擎的运行并没有任何影响,但是我们来分析下:

我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据。而B+树的每一层中的页都会形成一个双向链表,如果是以为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。

B+树的范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机I/O。磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O

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

2.段的概念

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

默认情况下一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M存储空间,所以默认情况下一个只存了几条记录的小表也需要2M的存储空间么?以后每次添加一个索引都要多申请2M的存储空间么?

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

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

所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面以及一些完整的区的集合。除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段,当然我们现在并不关心别的类型的段,现在只需要知道段是一些零散的页面以及一些完整的区的集合就好了。

有的时候处于不同的阶段,对于某个概念的定义或者理解是不同的,随着知识水平的提升后续再来逐渐完善,就像小学的时候老师会告诉你最小的数是0,中学又告诉你最小的数是负无穷一样。

3.区的分类

每个区都对应一个XDES Entry结构,这个结构中存储了一些与这个区有关的属性。这些区可以被分为下面四种类型。

  1. 空闲的区:现在还没有用到这个区中的任何页面,这些区会被加入到FREE链表。
  2. 有剩余空间的碎片区:表示碎片区中还有可用的页面,这些区会被加入到FREE_FRAG链表。
  3. 没有剩余空间的碎片区:表示碎片区中的所有页面都被使用,没有空闲页面;这些区会被加入到FULL_FRAG链表。
  4. 附属于某个段的区。每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位;每个段所属的区又会被组织成下面几种链表。
    1. FREE链表:在同一个段中,所有页面都是空闲页面的区对应的XDES Entry结构会被加入到这个链表。
    2. NOT_FULL链表:在同一个段中,仍有空闲页面的区对应的XDES Entry结构会被加入到这个链表。
    3. FULL链表:在同一个段中,已经没有空闲页面的区对应的XDES Entry结构会被加入到这个链表。

这四种类型的区也被叫做区的四种状态。

状态名 含义
FREE 空闲的区
FREE_FRAG 有剩余空间的碎片区
FULL_FRAG 没有剩余空间的碎片区
FSEG 附属于某个段的区

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

每个段都会对应一个INODE Entry结构,该结构中存储了一些与这个段有关的属性。

表空间中第一个页面的类型为FSP_HDR,它存储了表空间的一些整体属性以及第一个组内256个区对应的XDES Entry结构。

除了表空间的第一个组以外,其余组的第一个页面的类型为XDES,这种页面的结构和FSP_HDR类型的页面对比,除了少了File Space header(记录表空间整体属性的部分)部分之外,其余部分是一样的。

每个组的第二个页面类型为IBUF_BITMAP,存储了一些关于Change Buffer的信息。

表空间中第一个组的第三个页面的类型是INODE,他是为了存储INODE Entry结构而设计的,这种类型的页面会组织成下面两个链表。

  1. SEG_INODES_FULL链表:在该链表中,INODE类型的页面中已经没有空闲空间来存储额外的INODE Entry结构。
  2. SEG_INODES_FREE链表:在该链表中,INODE类型的页面中还有空闲空间来存储额外的INODE Entry结构。

4. Segment Header

一个索引会产生两个段,分别是叶子节点段和非叶子节点段,而每个段都会对应一个INODE Entry结构,那我们怎么知道某个段对应哪个**INODE Entry**结构呢?所以得找个地方记下来这个对应关系。INDEX类型的页时有一个Page Header部分,其中的PAGE_BTR_SEG_LEAFPAGE_BTR_SEG_TOP都占用10个字节,它们其实对应一个叫Segment Header的结构,该结构图示如下:

SegmentHeader结构.png

各个部分的具体释义如下:

名称 占用字节数 描述
Space ID of the INODE Entry 4 INODE Entry结构所在的表空间ID
Page Number of the INODE Entry 4 INODE Entry结构所在的页面页号
Byte Offset of the INODE Ent 2 INODE Entry结构在该页面中的偏移量

PAGE_BTR_SEG_LEAF记录着叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量,PAGE_BTR_SEG_TOP记录着非叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量。这样子索引和其对应的段的关系就建立起来了。不过需要注意的一点是,因为一个索引只对应两个段,所以只需要在索引的根页面中记录这两个结构即可。

其实Segment Header的作用就是记录哪个段对应哪个INODE Entry结构的。

5. 真实表空间对应的文件大小

一个新建的表对应的.ibd文件只占用了96KB,才6个页的大小。刚开始的时候,表空间占用空间自然很小,因为表里面没有数据。不过,ibd文件是自扩展文件,随着数据的增多文件也在逐渐增大。

四,系统表空间

系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些记录这些信息的页面。因为这个系统表空间相当于是表空间之首,所以它的表空间 ID(Space ID)是0

1.系统表空间的整体结构

系统表空间与独立表空间的一个非常明显的不同之处就是在表空间开头有许多记录整个系统属性的页面。

系统表空间结构.png

可以看到,系统表空间和独立表空间的前三个页面(页号分别为012,类型分别是FSP_HDRIBUF_BITMAPINODE)的类型是一致的,只是页号为37的页面是系统表空间特有的,我们来看一下这些多出来的页面都是干啥使的:

页号 页面类型 英文描述 描述
3 SYS Insert Buffer Header 存储Insert Buffer的头部信息
4 INDEX Insert Buffer Root 存储Insert Buffer的根页面
5 TRX_SYS Transaction System 事务系统的相关信息
6 SYS First Rollback Segment 第一个回滚段的页面
7 SYS Data Dictionary Header 数据字典头部信息

除了这几个记录系统属性的页面之外,系统表空间的extent 1extent 2这两个区,也就是页号从64~191这128个页面被称为Doublewrite buffer,也就是双写缓冲区。不过上述的大部分知识都涉及到了事务和多版本控制的问题,现在我们只分析有关InnoDB数据字典的知识,其余的概念在后边再看。

1.1 InnoDB数据字典

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

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

上述这些数据并不是我们使用INSERT语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得已引入的一些额外数据,这些数据也称为元数据。InnoDB存储引擎特意定义了一些列的内部系统表(internal system table)来记录这些这些元数据

表名 描述
SYS_TABLES 整个InnoDB存储引擎中所有的表的信息
SYS_COLUMNS 整个InnoDB存储引擎中所有的列的信息
SYS_INDEXES 整个InnoDB存储引擎中所有的索引的信息
SYS_FIELDS 整个InnoDB存储引擎中所有的索引对应的列的信息
SYS_FOREIGN 整个InnoDB存储引擎中所有的外键的信息
SYS_FOREIGN_COLS 整个InnoDB存储引擎中所有的外键对应列的信息
SYS_TABLESPACES 整个InnoDB存储引擎中所有的表空间信息
SYS_DATAFILES 整个InnoDB存储引擎中所有的表空间对应文件系统的文件路径信息
SYS_VIRTUAL 整个InnoDB存储引擎中所有的虚拟生成列的信息

这些系统表也被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中SYS_TABLESSYS_COLUMNSSYS_INDEXESSYS_FIELDS这四个表尤其重要,称之为基本系统表(basic system tables),我们先看看这4个表的结构:

1.2 SYS_TABLES表

列名 描述
name 表的名称
id InnoDB存储引擎每一张表都有一个唯一的ID
n_cols 该表拥有的列的个数
type 表的类型,记录了一些文件格式,行格式,压缩等信息
Mix_id 已经过时,忽略
Mix_len 表的一些额外属性
Cluster_id 未使用,忽略
Space 该表所属空间的ID

这个SYS_TABLES表有两个索引:

  • NAME列为主键的聚簇索引
  • ID列建立的二级索引

1.3 SYS_COLUMNS表

列名 描述
TABLE_ID 该列所属表对应的ID
POS 该列在表中是第几列
NAME 该列的名称
MTYPE main data type,主数据类型,就是那堆INT、CHAR、VARCHAR、FLOAT、DOUBLE等
PRTYPE precise type,精确数据类型,就是修饰主数据类型的那堆东东,比如是否允许NULL值,是否允许负数啥的
LEN 该列最多占用存储空间的字节数
PREC 该列的精度,不过这列貌似都没有使用,默认值都是0

这个SYS_COLUMNS表只有一个聚集索引:

  • (TABLE_ID, POS)列为主键的聚簇索引

1.4 SYS_INDEXES表

列名 描述
TABLE_ID 该索引所属表对应的ID
ID InnoDB存储引擎中每个索引都有一个唯一的ID
NAME 该索引的名称
N_FIELDS 该索引包含列的个数
TYPE 该索引的类型,比如聚簇索引、唯一索引、更改缓冲区的索引、全文索引、普通的二级索引等等各种类型
SPACE 该索引根页面所在的表空间ID
PAGE_NO 该索引根页面所在的页面号
MERGE_THRESHOLD 如果页面中的记录被删除到某个比例,就把该页面和相邻页面合并,这个值就是这个比例

这个SYS_INDEXES表只有一个聚集索引:

  • (TABLE_ID, ID)列为主键的聚簇索引

1.5 SYS_FIELDS表

列名 描述
INDEX_ID 该索引列所属的索引的ID
POS 该索引列在某个索引中是第几列
COL_NAME 该索引列的名称

这个SYS_FIELDS表只有一个聚集索引:

  • (INDEX_ID, POS)列为主键的聚簇索引

1.6 Data Dictionary Header页面

只要有了上述4个基本系统表,也就意味着可以获取其他系统表以及用户定义的表的所有元数据。比方说我们想看看SYS_TABLESPACES这个系统表里存储了哪些表空间以及表空间对应的属性,那就可以:

  • SYS_TABLES表中根据表名定位到具体的记录,就可以获取到SYS_TABLESPACES表的TABLE_ID
  • 使用这个TABLE_IDSYS_COLUMNS表中就可以获取到属于该表的所有列的信息。
  • 使用这个TABLE_ID还可以到SYS_INDEXES表中获取所有的索引的信息,索引的信息中包括对应的INDEX_ID,还记录着该索引对应的B+数根页面是哪个表空间的哪个页面。
  • 使用INDEX_ID就可以到SYS_FIELDS表中获取所有索引列的信息。

这4个表的元数据去哪里获取呢?这4个表的元数据,就是它们有哪些列、哪些索引等信息是硬编码到代码中的,InnoDB用一个固定的页面来记录这4个表的聚簇索引和二级索引对应的B+树位置,这个页面就是页号为7的页面,类型为SYS,记录了Data Dictionary Header,也就是数据字典的头部信息。除了这4个表的5个索引的根页面信息外,这个页号为7的页面还记录了整个InnoDB存储引擎的一些全局属性。

这个页面由下边几个部分组成:

名称 中文名 占用空间(字节) 简单描述
File Header 文件头部 38 页的一些通用信息
Data Dictionary Header 数据字典头部信息 56 记录一些基本系统表的根页面位置以及InnoDB存储引擎的一些全局信息
Segment Header 段头部信息 10 记录本页面所在段对应的INODE Entry位置信息
Empty Space 尚未使用空间 16272 用于页结构的填充,没啥实际意义
File Trailer 文件尾部 8 校验页是否完整

这个页面里有Segment Header部分,意味着InnoDB把这些有关数据字典的信息当成一个段来分配存储空间,我们称之为数据字典段。由于目前我们需要记录的数据字典信息非常少(可以看到Data Dictionary Header部分仅占用了56字节),所以该段只有一个碎片页,也就是页号为7的这个页。

页号为7的页结构示意图.png

接下来我们需要看一下Data Dictionary Header部分的各个字段:

  • Max Row ID:如果我们不显式的为表定义主键,而且表中也没有UNIQUE索引,那么InnoDB存储引擎会默认为我们生成一个名为row_id的列作为主键。因为它是主键,所以每条记录的row_id列的值不能重复。原则上只要一个表中的row_id列不重复就可以了,也就是说表a和表b拥有一样的row_id列也没啥关系,不过InnoDB只提供了这个Max Row ID字段,不论哪个拥有row_id列的表插入一条记录时,该记录的row_id列的值就是Max Row ID对应的值,然后再把Max Row ID对应的值加1,也就是说这个Max Row ID是全局共享的。
  • Max Table ID:InnoDB存储引擎中的所有的表都对应一个唯一的ID,每次新建一个表时,就会把本字段的值作为该表的ID,然后自增本字段的值。
  • Max Index ID:InnoDB存储引擎中的所有的索引都对应一个唯一的ID,每次新建一个索引时,就会把本字段的值作为该索引的ID,然后自增本字段的值。
  • Max Space ID:InnoDB存储引擎中的所有的表空间都对应一个唯一的ID,每次新建一个表空间时,就会把本字段的值作为该表空间的ID,然后自增本字段的值。
  • Mix ID Low(Unused):这个字段没啥用,跳过。
  • Root of SYS_TABLES clust index:本字段代表SYS_TABLES表聚簇索引的根页面的页号。
  • Root of SYS_TABLE_IDS sec index:本字段代表SYS_TABLES表为ID列建立的二级索引的根页面的页号。
  • Root of SYS_COLUMNS clust index:本字段代表SYS_COLUMNS表聚簇索引的根页面的页号。
  • Root of SYS_INDEXES clust index本字段代表SYS_INDEXES表聚簇索引的根页面的页号。
  • Root of SYS_FIELDS clust index:本字段代表SYS_FIELDS表聚簇索引的根页面的页号。
  • Unused:这4个字节没用,跳过。

以上就是页号为7的页面的全部内容。

1.7 information_schema系统数据库

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

  1. mysql> USE information_schema;
  2. Database changed
  3. mysql> SHOW TABLES LIKE 'innodb_sys%';
  4. +--------------------------------------------+
  5. | Tables_in_information_schema (innodb_sys%) |
  6. +--------------------------------------------+
  7. | INNODB_SYS_DATAFILES |
  8. | INNODB_SYS_VIRTUAL |
  9. | INNODB_SYS_INDEXES |
  10. | INNODB_SYS_TABLES |
  11. | INNODB_SYS_FIELDS |
  12. | INNODB_SYS_TABLESPACES |
  13. | INNODB_SYS_FOREIGN_COLS |
  14. | INNODB_SYS_COLUMNS |
  15. | INNODB_SYS_FOREIGN |
  16. | INNODB_SYS_TABLESTATS |
  17. +--------------------------------------------+
  18. 10 rows in set (0.00 sec)

information_schema数据库中的这些以INNODB_SYS开头的表并不是真正的内部系统表,而是在存储引擎启动时读取这些以SYS开头的系统表,然后填充到这些以INNODB_SYS开头的表中。以INNODB_SYS开头的表和以SYS开头的表中的字段并不完全一样。

补充一张表空间完整结构图

表空间完整结构图.jpg