作者:木木
链接:https://www.zhihu.com/question/26398102/answer/3021842336
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
从本文开始,我将分享一下近期学习 MySQL 的笔记,其中大部分来源于极客时间的《MySQL实战45讲》、小林coding、以及部分其他博客和书籍。
这次系列文章着重讲 MySQL 的原理部分,主要是用于面试,也就是我们常说的八股,至于基础的内容就不再介绍了。我将在后续文章中陆续介绍以下内容:存储结构、逻辑架构和执行流程、索引、事务、锁、日志、内存管理、磁盘空间管理、集群相关等。
另外,如果没有额外说明,介绍的主要内容都是关于默认的 InnoDB 引擎。
接下来开始正式讲解逻辑存储架构相关内容。我们都知道,MySQL 是将数据存储到硬盘、磁盘等外部存储器上的,逻辑存储架构,指的就是 MySQL 是如何组织、存储数据的。
InnoDB 的所有数据都被逻辑地存放在表空间(tablespace)中,表空间又由段(segment),区/簇(extent),页(page)组成。
System Tablespace,系统表空间可以对应文件系统上一个或多个实际的文件,默认情况下,InnoDB 会在数据目录下创建一个名为 ibdata1 的文件,大小为 12MB,这个文件就是对应的系统表空间在文件系统上的表示。这个文件是自扩展文件,当不够用的时候它会自己增加文件大小。在一个 MySQL 服务器中,系统表空间只有一份。
InnoDB 有四个最基本的系统表:
系统表空间与独立表空间的一个非常明显的不同之处就是在表空间开头有许多记录整个系统属性的页面:
File-per-table Tablespace,在 MySQL5.6.6 之后,InnoDB 并不会默认地把各个表的数据存储到系统表空间中,而是为每一个表建立一个独立表空间。创建一个表后,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名为 表名.ibd
。
可以指定使用系统表空间还是独立表空间来存储数据,这个功能由启动参数 innodb_file_per_table 控制,在 my.cnf 配置文件中。
独立表空间整体结构如下:
段不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面(碎片区)和一系列完整的区组成。
一个表空间里面会有很多个段组成。常见的段有数据段、索引段、回滚段等。
每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的 B+ 树的节点中插入数据。
InnoDB 对 B+ 树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段,存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成两个段,一个叶子节点段和一个非叶子节点段。
每个索引段的大小是固定的,通常为 16KB。如果索引键的大小超过了 16KB,则需要使用多个索引段来存储。
InnoDB 为每个段都定义了一个 INODE Entry 结构来记录段中的属性。
InnoDB 为每个段中的区对应的 XDES Entry 结构建立了三个链表:
每个段都会维护上述的三个链表。
不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每 256 个区被划分成一组。对于 16KB 的页来说,连续的 64 个页就是一个区,也就是说一个区默认占用 1MB 空间大小。
一个区中的页,在物理空间中是连续的,所以在读取时可以减少随机 IO 的次数,从而提高读取速度。
第一个组最开始的三个页面的类型是固定的,分别是:
其余各组最开始的两个页面的类型是固定的,分别是:
为了方便管理区,MySQL 设计了 XDES Entry(Extent Descriptor Entry)结构,每个区都对应着一个 XDES Entry,这个结构记录了对应的区的一些属性。
XDES Entry 结构有 40 字节,大致分为 4 个部分:
段是以区为单位申请存储空间的,一个区默认占用 1MB 的空间,如果为段中很小的数据量申请区,则会造成大量空间的浪费,所以 InnoDB 提出了碎片区的概念,碎片区中的页并不都是为了存储同一个段中的数据而存在的,比如有的页用于段 A,有的用于段 B,有的页甚至哪个段也不属于。碎片区直属于表空间,并不属于任何一个段。
为段分配存储空间的策略是:
记录是按照行来存储的,但是数据库的读取并不是以行为单位的。
InnoDB 的数据是按数据页为单位来读写的,即数据库的 I/O 操作的最小单位是页,是磁盘和内存之间交互的基本单位,默认大小为 16KB。
在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表。
数据页中的记录(User Record)按照主键顺序组成单向链表。
数据页中有一个页目录,起到记录的索引作用。
每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段)
页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。
InnoDB 对每个分组中的记录条数都是有规定的,槽内的记录就只有几条:
MySQL 有四种行格式:Redundant,Compact,Dynamic,Compressed。我们可以在创建表的时候指定字符集和行格式。
MySQL 5.7 默认使用的是 Dynamic 的行格式。
Redundant 是 MySQL5.0 版本之前的行格式,现在一般不会使用,只有 MySQL 的一些系统表会使用。
整个行格式分为记录的额外信息和记录的真实数据两部分,其中记录的额外信息又分为两部分:
名称 | 大小(bit) | 描述 |
---|---|---|
预留位1 | 1 | 未使用 |
预留位2 | 1 | 未使用 |
delete_mask | 1 | 该记录是否删除 |
min_rec_mask | 1 | B+树每层非叶子节点小记录都会添加该标记 |
n_owned | 4 | 当前记录组拥有记录数 |
heap_no | 13 | 当前记录在页面堆位置信息 |
n_field | 10 | 记录中列数量 |
1byte_offs_flag | 1 | 字段长度偏移列表中每个列对应的偏移量是使用1字节还是2字节表示的 |
next_record | 16 | 下一条记录的相对位置 |
记录的真实数据除了列数据以外,还会为每个记录默认添加一些列(也称为隐藏列),具体如下:
Compact 是一种紧凑的行格式,设计的初衷就是为了让一个数据页中可以存放更多的数据行。
Compact 描述信息的格式如下:
变长字段长度列表中存储的是每个非空的变长字段的数据长度,同样也是逆序存放的。如果表中没有变长的数据类型的话,就不需要该列表了。如果长度小于 255B,就用 1B 表示,否则用 2B 表示,最大不会超过 2B,因为 MySQL 中变长类型的最大字节长度限制为 65535。
如果表中没有允许 NULL 的列,那么 NULL 标记位就不存在了。否则将每个允许存储 NULL 的列对应一个二进制位并且逆序存放,然后将允许为 NULL 的位标为 1,不允许的标为 0;且必须用整数个字节的位表示,否则在高位补 0。
Compact 格式的记录头具体信息如下:
名称 | 大小(bit) | 描述 |
---|---|---|
预留位1 | 1 | 未使用 |
预留位2 | 1 | 未使用 |
delete_mask | 1 | 该记录是否删除 |
min_rec_mask | 1 | B+树每层非叶子节点小记录都会添加该标记 |
n_owned | 4 | 当前记录组拥有记录数 |
heap_no | 13 | 当前记录在页面堆位置信息 |
record_type | 3 | 表⽰当前记录的类型,0表⽰普通记录,1表⽰B+树⾮叶⼦节点记录,2表⽰⼩记录,3表⽰⼤记录 |
next_record | 16 | 下一条记录的相对位置 |
相比 Redundant,多了一个 record_type 字段,少了 n_field 和 1byte_offs_flag 两个字段。
Compact 的隐藏列和 Redundant 相同。
Compact 格式下,具有 65535 字节的最大行大小限制,但是设置允许 NULL 需要占用 1 个字节,设置为变长(varchar)需要占用两个字节。在不同的编码格式下,一个字符占用的字节是不同的,比如 encode=utf8 时,三个字节表示一个字符。
数据页默认大小为 16KB,即 16*1024=16384byte,所以当要存储地单行数据超过 16384byte 时,会发生行溢出。
但在 MySQL 的设定中,会提早发生行溢出。当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时,该列超过 768byte 的数据溢出存放到溢出页中,然后通过一个偏移量指针将两者关联起来,这就是行溢出机制。这样可以有效地防止单个列太大导致单个数据页中存放的行记录过少而让 IO 飙升的情况。
Compact 能比 Redundant 格式节约 20% 的存储空间。
Dynamic 和 Compact 行格式类似,只是在处理行溢出数据时有区别。Dynamic 的数据页不会存储真实数据的前 768 字节,而是存储 20 个字节的指针来指向溢出页,即将所有的溢出数据都存在溢出页中。
Compressed 与 Dynamic 相似,区别是 Compressed 存储的行数据会以 zlib 的算法进行压缩以节省空间,因此对于 BLOB、TEXT、VARCHAR 这类大长度类型的数据能够进行非常有效的存储。
系统表中保存了许多额外的信息,包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容,这些数据统称为元数据(元信息),这些元数据的集合(系统表)就成为数据字典。
在 MySQL8.0 以前,数据字典是以文件的形式存储的,在 MySQL8.0 以后,全都存放到数据库系统表中了。
本文介绍了 MySQL 底层的逻辑存储结构,每一条数据都是一个数据行,数据行是存储数据的基本单位。若干条数据会组成一个数据页,数据页是读写的基本单位,可以减少磁盘 IO 的次数。若干个连续的页会组成一个区,可以降低随机 IO 的次数,来提高查询效率,同时也便于进行管理。不同的区可以按照逻辑分布到不同的段中,可以更好地进行数据管理以及提高空间利用率。
下一节将介绍 MySQL 的逻辑架构和 SQL 语句的具体执行流程。