8. MySQL的数据目录
数据库和文件系统的关系
像 InnoDB 、 MyISAM 这样的存储引擎都是把表存储在文件系统上的。当我们想读取数据的时候,这些存储引擎会从文件系统中把数据读出来返回给我们,当我们想写入数据的时候,这些存储引擎会把这些数据又写回文件系统。
MySQL数据目录
:::tips MySQL服务器程序在启动时会到文件系统的某个目录下加载一些文件,之后在运行过程中产生的数据也都会存储到这个目录下的某些文件中,这个目录就称为 数据目录 ::: 安装目录是MySQL安装的目录, 下面有非常重要的 bin 目录,它里边存储了许多关于控制客户端程序和服务器程序的命令(许多可执行文件,比如 mysql , mysqld , mysqld_safe 等等等等好几十个)。
如何查看?
数据目录 对应着一个系统变量 datadir
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
数据目录的结构
数据库在文件系统中的表示
我们每当我们新建一个数据库时, MySQL 会帮我们做这两件事儿:
- 在 数据目录 下创建一个和数据库名同名的子目录(或者说是文件夹)。
在该与数据库名同名的子目录下创建一个名为
db.opt
的文件,这个文件中包含了该数据库的各种属性,比方说该数据库的字符集和比较规则是个啥。表在文件系统中的表示
表的信息有两种:
表结构的定义
- 表中的数据
InnoDB 和MyISAM 这两种存储引擎都在 数据目录 下对应的数据库子目录下创建了一个专门用于描述表结构的文件,文件名是这样:表名.frm
:::info
这个后缀名为.frm是以二进制格式存储的,我们直接打开会是乱码的
:::
如何存储表数据?
InnoDB的设计者提出了一个 表空间 或者 文件空间 (英文名: table space 或者 file space )的概念,这个表空间是一个抽象的概念,它可以对应文件系统上一个或多个真实文件(不同表空间对应的文件数量可能不同)。每一个 表空间 可以被划分为很多很多很多个 页 ,我们的表数据就存放在某个 表空间 下的某些页里。
系统表空间
在一个MySQL服务器中,系统表空间只有一份。从MySQL5.5.7到MySQL5.6.6之间的各个版本中,我们表中的数据都会被默认存储到这个 系统表空间。
独立表空间
在MySQL5.6.6以及之后的版本中, InnoDB 并不会默认的把各个表的数据存储到系统表空间中,而是为每一个表建立一个独立表空间,也就是说我们创建了多少个表,就有多少个独立表空间
视图
MySQL 中的视图其实是虚拟的表,也就是某个查询语句的一个别名而已,所以在存储 视图 的时候是不需要存储真实的数据的,只需要把它的结构存储起来就行了。和 表 一样,描述视图结构的文件也会被存储到所属数据库对应的子目录下边,只会存储一个 视图名.frm
的文件
MySQL系统数据库简介
- mysql:它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等
- information_schema:MySQL服务器维护的所有其他数据库的信息,比如有哪些表、哪些视图、哪些触发器、哪些列、哪些索引
- performance_schema:这个数据库里主要保存MySQL服务器运行过程中的一些状态信息,算是对MySQL服务器的一个性能监控。包括统计最近执行了哪些语句。
9. InnoDB的表空间
:::tips 对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为 表名.ibd 的实际文件。大家可以把表空间想象成被切分为许许多多个 页 的池子,当我们想为某个表插入一条记录的时候,就从池子中捞出一个对应的页来把数据写进去。 :::复习
不同的页面类型:
类型名称 | 十六进制 | 描述 |
---|---|---|
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还没使用 |
FIL_PAGE_UNDO_LOG | 0x0002 | Undo日志页 |
FIL_PAGE_INODE | 0x0003 | 段信息节点 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB页 |
FIL_PAGE_INDEX | 0x45BF | 索引页,也就是我们所说的 数据页 |
- File Header :记录页面的一些通用信息
- File Trailer :校验页是否完整,保证从内存到磁盘刷新时内容的一致性。
独立表空间
区的概念(extent)
:::info 对于16KB的页来说,连续的64个页就是一个 区 ,也就是说一个区默认占用_1_MB空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。 :::
这些组的头几个页面的类型都是类似的
第一个组最开始的3个页面的类型是固定的,也就是说 extent 0 这个区最开始的3个页面的类型是固定的,
分别是:
FSP_HDR
类型:这个类型的页面是用来登记整个表空间的一些整体属性以及本组所有的 区 ,也就是extent 0 ~ extent 255 这256个区的属性,稍后详细唠叨。需要注意的一点是,整个表空间只有一个**FSP_HDR**
类型的页面。IBUF_BITMAP
类型:这个类型的页面是存储本组所有的区的所有页面关于 INSERT BUFFER 的信息。INODE
类型:这个类型的页面存储了许多称为INODE
的数据结构
其余各组最开始的2个页面的类型是固定的,也就是说 extent 256 、 extent 512 这些区最开始的2个页面
的类型是固定的,分别是:
XDES
类型:全称是 extent descriptor ,用来登记本组256个区的属性,也就是说对于在 extent 256区中的该类型页面存储的就是 extent 256 ~ extent 511 这些区的属性,上边介绍的FSP_HDR
类型的页面其实和XDES
类型的页面的作用类似,只不过 FSP_HDR 类型的页面还会额外存储一些表空间的属性。-
段的概念(segment)
为何要引入区和段呢?不用区和段也是可以存储的,但是在数据量较多的情况下,引入区和段可以显著提高效率。
引入区的概念是为了尽量使得查找变成顺序IO
我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的 B+ 树的节点中插入数据。而 B+ 树的每一层中的页都会形成一个双向链表,如果是以 页 为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。我们介绍 B+ 树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的 随机I/O 。再一次强调,磁盘的速度和内存的速度差了好几个数量级, 随机I/O 是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的 顺序I/O 。 :::tips 一个区就是在物理位置上连续的64个页。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照 区 为单位分配。虽然可能造成一点点空间的浪费(数据不足填充满整个区),但是从性能角度看,可以消除很多的随机 I/O :::
引入段的概念是为了区分子叶节点和非子叶节点
:::info 如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣。why? ::: InnoDB对 B+ 树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的 区 ,非叶子节点也有自己独有的 区 。存放叶子节点的区的集合就算是一个 段( segment ),存放非叶子节点的区的集合也算是一个 段 。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。
碎片区
段是以区为单位申请存储空间的,一个区默认占用1M存储空间,对于数据量较小的表来说直接申请段会造成很大的浪费。 :::info 碎片(fragment)区的概念:
在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。 :::真正的分配空间的策略
在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
- 当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间。(区是64个,相当于过半就分配!)
区的分类
- 空闲的区:现在还没有用到这个区中的任何页面。
- 有剩余空间的碎片区:表示碎片区中还有可用的页面。
- 没有剩余空间的碎片区:表示碎片区中的所有页面都被使用,没有空闲页面。
- 附属于某个段的区。每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。 | 状态名 | 含义 | | —- | —- | | FREE | 空闲的区 | | FREE_FRAG | 有剩余空间的碎片区 | | FULL_FRAG | 没有剩余空间的碎片区 | | FSEG | 附属于某个段的区 |
XDES Entry
每一个区都对应着一个XDES Entry
结构,这个结构记录了对应的区的一些属性
Segment ID (8字节) :::tips 每一个段都有一个唯一的编号,用ID表示,此处的 Segment ID 字段表示就是该区所在的段。 :::
List Node (12字节) :::tips 这个部分可以将若干个 XDES Entry 结构串联成一个链表 ::: 如果我们想定位表空间内的某一个位置的话,只需指定页号以及该位置在指定页号中的页内偏移量即可。所以:
- Pre Node Page Number 和 Pre Node Offset 的组合就是指向前一个 XDES Entry 的指针
- Next Node Page Number 和 Next Node Offset 的组合就是指向后一个 XDES Entry 的指针。
- State (4字节)
这个字段表明区的状态。可选的值就是我们前边说过的那4个,分别是: FREE 、 FREE_FRAG 、 FULL_FRAG
和 FSEG 。
- Page State Bitmap (16字节)
这个部分共占用16个字节,也就是128个比特位。我们说一个区默认有64个页,这128个比特位被划分为64个部分,每个部分2个比特位,对应区中的一个页。比如 Page State Bitmap 部分的第1和第2个比特位对应着区中的第1个页面。这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。
XDES Entry链表
向某个段插入数据的过程? :::success
- 数据较少的时候,先查找有空闲页(FREE_FRAG)的区,如果有则插入
- 否则申请新的空闲区(FREE)并转换其状态为(FREE_FRAG),然后插入。
- 之后不同的段使用零碎页的时候都会从该区中取,直到该区中没有空闲空间,然后该区的状态就变成了FULL_FRAG 。
- 当段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了
:::
现在的问题是你怎么知道表空间里的哪些区是 FREE 的,哪些区的状态是 FREE_FRAG 的?
遍历一遍链表?不可能!太慢了
我们可以通过 List Node 中的指针
- 把状态为 FREE 的区对应的 XDES Entry 结构通过 List Node 来连接成一个链表,这个链表我们就称之为 FREE 链表。
- 把状态为 FREE_FRAG 的区对应的 XDES Entry 结构通过 List Node 来连接成一个链表,这个链表我们就称之为 FREE_FRAG 链表。
- 把状态为 FULL_FRAG 的区对应的 XDES Entry 结构通过 List Node 来连接成一个链表,这个链表我们就称之为 FULL_FRAG 链表。 :::info 这样每当我们想找一个 FREE_FRAG 状态的区时,就直接把 FREE_FRAG 链表的头节点拿出来,从这个节点中取一些零碎的页来插入数据,当这个节点对应的区用完时,就修改一下这个节点的 State 字段的值,然后从 FREE_FRAG 链表中移到 FULL_FRAG 链表中 :::
当段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了
我们怎么知道哪些区属于哪个段的呢?通过链表:
每个段中的区对应的 XDES Entry 结构建立了三个链表:
- FREE 链表:同一个段中,所有页面都是空闲的区对应的 XDES Entry 结构会被加入到这个链表。注意和直属于表空间的 FREE 链表区别开了,此处的 FREE 链表是附属于某个段的。
- NOT_FULL 链表:同一个段中,仍有空闲空间的区对应的 XDES Entry 结构会被加入到这个链表。
FULL 链表:同一个段中,已经没有空闲空间的区对应的 XDES Entry 结构会被加入到这个链表。 :::success 一个问题:之前存在碎片区的32个页呢?不管了?还是把他移到新的区内?因为之前那32个页不一定是连续的物理地址空间。
回想之前的定义:段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。 :::链表基节点
怎么找到某个链表的头节点或者尾节点在表空间中的位置呢? :::tips 依靠一个叫 List Base Node 的结构,翻译成中文就是链表的基节点。这个结构中包含了链表的头节点和尾节点的指针以及这个链表中包含了多少节点的信息
:::List Length 表明该链表一共有多少节点,
- First Node Page Number 和 First Node Offset 表明该链表的头节点在表空间中的位置。
- Last Node Page Number 和 Last Node Offset 表明该链表的尾节点在表空间中的位置。
Summary
表空间是由若干个区组成的,每个区都对应一个 XDES Entry 的结构,直属于表空间的区(碎片区)对应的 XDES Entry 结构可以分成 FREE 、 FREE_FRAG 和 FULL_FRAG 这3个链表;
每个段可以附属若干个区,每个段中的区对应的 XDES Entry 结构可以分成 FREE 、 NOT_FULL 和 FULL 这3个链表。每个链表都对应一个 List Base Node 的结构,这个结构里记录了链表的头、尾节点的位置以及该链表中包含的节点数。段的结构
每个段都定义了一个 INODE Entry 结构来记录一下段中的属性
- Segment ID:就是指这个 INODE Entry 结构对应的段的编号(ID)。
- NOT_FULL_N_USED:这个字段指的是在 NOT_FULL 链表中已经使用了多少个页面。下次从 NOT_FULL 链表分配空闲页面时可以直接根据这个字段的值定位到。而不用从链表中的第一个页面开始遍历着寻找空闲页面。
- 3个 List Base Node:分别为段的 FREE 链表、 NOT_FULL 链表、 FULL 链表定义了 List Base Node ,这样我们想查找某个段的某个链表的头节点和尾节点的时候,就可以直接到这个部分找到对应链表的 List Base Node 。
- Magic Number :这个值是用来标记这个 INODE Entry 是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。如果这个数字是值的 97937874 ,表明该 INODE Entry 已经初始化,否则没有被初始化。(不用纠结这个值有啥特殊含义,人家规定的)。
- Fragment Array Entry:我们前边强调过无数次段是一些零散页面和一些完整的区的集合,每个 Fragment Array Entry 结构都对应着一个零散的页面,这个结构一共4个字节,表示一个零散页面的页号。
各类型页面详细情况
FSP_HDR 类型
它存储了表空间的一些整体属性以及第一个组内256个区的对应的 XDES Entry 结构
名称 | 中文名 | 占用空间 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38 字节 | 页的一些通用信息 |
File Space Header | 表空间头部 | 112 字节 | 表空间的一些整体属性信息 |
XDES Entry | 区描述信息 | 10240 字节 | 存储本组256个区对应的属性信息 |
Empty Space | 尚未使用空间 | 5986 字节 | 用于页结构的填充,没啥实际意义 |
File Trailer | 文件尾部 | 8 字节 | 校验页是否完整 |
- FRAG_N_USED:FREE_FRAG链表中已使用的页面数量
- List Base Node for FREE List:FREE链表的基节点
- List Base Node for FREE_FRAG List:FREE_FREG链表的基节点
- List Base Node for FULL_FRAG List:FULL_FREG链表的基节点
XDES类型
一个 XDES Entry 结构的大小是40字节,但是一个页面的大小有限,只能存放有限个 XDES Entry 结构,所以我们才把256个区划分成一组,在每组的第一个页面中存放256个 XDES Entry 结构。
IBUF_BITMAP 类型
TBCINODE 类型
每个段都有一个INODE Entry结构来描述段的信息。 :::tips INODE 类型的页就是为了存储 INODE Entry 结构而存在的
:::
- List Node for INODE Page List:通用链表节点,12 字节存,储上一个INODE页面和下一个INODE页面的指针
- INODE Entry:段描述信息,16128 字节
因为一个表空间中可能存在超过85个段,所以可能一个 INODE 类型的页面不足以存储所有的段对应的 INODE Entry 结构,所以就需要额外的 INODE 类型的页面来存储这些结构。还是为了方便管理这些 INODE 类型的页面,这些 INODE 类型的页面串联成两个不同的链表:
- SEG_INODES_FULL 链表:该链表中的 INODE 类型的页面中已经没有空闲空间来存储额外的 INODE Entry 结构了。
SEG_INODES_FREE 链表:该链表中的 INODE 类型的页面中还有空闲空间来存储额外的 INODE Entry 结构了。
Segment Header 结构的运用
一个索引会产生两个段,分别是叶子节点段和非叶子节点段,而每个段都会对应一个 INODE Entry 结构,那我们怎么知道某个段对应哪个 INODE Entry 结构呢?所以得找个地方记下来这个对应关系——>Segment Header
在页的Page Header 中存储着两个结构PAGE_BTR_SEG_LEAF
:10 字节,B+树叶子段的头部信息,仅在B+树的根页定义PAGE_BTR_SEG_TOP
:10 字节,B+树非叶子段的头部信息,仅在B+树的根页定义
它们其实对应一个叫 Segment Header 的结构
名称 | 占用字节数 | 描述 |
---|---|---|
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结构在该页面中的偏移量 |
因为一个索引只对应两个段,所以只需要在索引的根页面中记录这两个结构即可
系统表空间
系统表空间与独立表空间的一个非常明显的不同之处就是在表空间开头有许多记录整个系统属性的页面
页号 | 页面类型 | 英文描述 | 描述 |
---|---|---|---|
3 | SYS | Insert Buffer Header | 存储Insert Buffer的头部信息 |
4 | INDEX | Insert Buffer Root | 存储Insert Buffer的根页面 |
5 | TRX_SYS | Transction System | 事务系统的相关信息 |
6 | SYS | First Rollback Segment | 第一个回滚段的页面 |
7 | SYS | Data Dictionary Header | 数据字典头部信息 |
InnoDB数据字典
MySQL除了保存着我们插入的用户数据之外,还需要保存许多额外的信息,如:
- 某个表属于哪个表空间,表里边有多少列
- 表对应的每一个列的类型是什么
- 该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面
- 该表有哪些外键,外键对应哪个表的哪些列
- ……
元数据
:::info 上述这些数据并不是我们使用 INSERT 语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得已引入的一些额外数据,这些数据也称为 元数据 。 :::内部系统表
InnoDB存储引擎特意定义了一些列的内部系统表(internal system table)来记录这些这些 元数据
表名 | 描述 |
---|---|
SYS_TABLES | 整个InnoDB存储引擎中所有的表的信息 |
SYS_COLUMNS | 整个InnoDB存储引擎中所有的列的信息 |
SYS_INDEXES | 整个InnoDB存储引擎中所有的索引的信息 |
SYS_FIELDS | 整个InnoDB存储引擎中所有的索引对应的列的信息 |
SYS_FOREIGN | 整个InnoDB存储引擎中所有的外键的信息 |
这些系统表也被称为 数据字典 ,它们都是以 B+ 树的形式保存在系统表空间的某些页面中,其中SYS_TABLES 、 SYS_COLUMNS 、 SYS_INDEXES 、 SYS_FIELDS 这四个表尤其重要,称之为基本系统表(basic system tables)
Data Dictionary Header页面
10. 单表访问方法
例如对于下面一个表
CREATE TABLE single_table (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
访问方法(access method)的概念
查询的执行方式大致分为下边两种:
- 使用全表扫描进行查询
- 使用索引进行查询
MySQL 执行查询语句的方式称之为 访问方法 或者 访问类型 。同一个查询语句可能可以使用多种不同的访问方法来执行,虽然最后的查询结果都是一样的,但是执行的时间可能相差极大。
const
:::tips 通过主键或者唯一二级索引列与常数的等值比较来定位一条记录是极快的,这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为: const ,意思是常数级别的,代价是可以忽略不计的。 ::: 通过主键查询:
- 根据聚簇索引二分法查找
唯一二级索引:
- 根据二级索引二分查找
根据找到的主键回表,再来一次二分查找 :::danger 唯一特例是查询NULL值
SELECT * FROM single_table WHERE key2 IS NULL;
因为唯一二级索引列并不限制 NULL 值的数量,所以上述语句可能访问到多条记录,也就是说 上边这个语句不可以使用 const 访问方法来执行 :::ref
由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。如果匹配的记录较少,则回表的代价还是比较低的,所以 MySQL 可能选择使用索引而不是全表扫描的方式来执行查询。
:::danger不论是普通的二级索引,还是唯一二级索引,它们的索引列对包含 NULL 值的数量并不限制,所以我们采用key IS NULL 这种形式的搜索条件最多只能使用 ref 的访问方法,而不是 const 的访问方法
- 只要是最左边的连续索引列是与常数的等值比较就可能采用 ref的访问方法
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary';
- 但是如果最左边的连续索引列并不全部是等值比较的话,它的访问方法就不能称为 ref 了
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 > 'legendary';
:::
ref_or_null
有时候我们不仅想找出某个二级索引列的值等于某个常数的记录,还想把该列的值为 NULL 的记录也找出来SELECT * FROM single_demo WHERE key1 = 'abc' OR key1 IS NULL;
range
SELECT * FROM single_table WHERE key2 IN (1438, 6328) OR (key2 >= 38 AND key2 <= 79);
采用 二级索引 + 回表 的方式来执行的话,此时的搜索条件就不只是要求索引列与常数的等值匹配了,而是索引列需要匹配某个或某些范围的值
Index
SELECT key_part1, key_part2, key_part3 FROM single_table WHERE key_part2 = 'abc';
满足下列条件:
- 它的查询列表只有3个列: key_part1 , key_part2 , key_part3 ,而索引 idx_key_part 又包含这三个列。
- 搜索条件中只有 key_part2 列。这个列也包含在索引 idx_key_part 中。
:::tips
- 可以直接通过遍历 idx_key_part 索引的叶子节点的记录来比较 key_part2 = ‘abc’ 这个条件是否成立,把匹配成功的二级索引记录的 key_part1 , key_part2 , key_part3 列的值直接加到结果集中就行了。
- 而且这个过程也不用进行回表操作,所以直接遍历二级索引比直接遍历聚簇索引的成本要小很多
:::
all
最直接的查询执行方式就是我们已经提了无数遍的全表扫描,对于 InnoDB 表来说也就是直接扫描聚簇索引
注意事项
一般情况下只能利用单个二级索引执行查询
e.g. SELECT * FROM single_table WHERE key1 = 'abc' AND key2 > 1000;
优化器一般会根据 single_table 表的统计数据来判断到底使用哪个条件到对应的二级索引中查询扫描的行数会更少,选择那个扫描行数较少的条件到对应的二级索引中查询.
用range时要注意优化
注意两种情况:
- 从 idx_key1 二级索引对应的 B+ 树中取出 key1 = ‘a’ 的相关记录。
- 从 idx_key3 二级索引对应的 B+ 树中取出 key3 = ‘b’ 的相关记录。
二级索引的记录都是由 索引列 + 主键 构成的,所以我们可以计算出这两个结果集中 id 值的交集。按照上一步生成的 id 值列表进行回表操作,也就是从聚簇索引中把指定 id 值的完整用户记录取出来,返回给用户 :::info 读取二级索引的操作是 顺序I/O ,而回表操作是 随机I/O ,所以如果只读取一个二级索引时需要回表的记录数特别多,而读取多个二级索引之后取交集的记录数非常少 ::: MySQL 在某些特定的情况下才可能会使用到 Intersection 索引合并:
情况一:二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只出现匹配部分列的情况。
SELECT * FROM single_table WHERE key1 = 'a' AND key_part1 = 'a' AND key_part2 = 'b' AND key_part3 = 'c';
- 情况二:主键列可以是范围匹配
SELECT * FROM single_table WHERE id > 100 AND key1 = 'a';
Union合并
Union 是并集的意思,适用于使用不同索引的搜索条件之间使用 OR 连接起来的情况。与 Intersection 索引合并类似,MySQL 在某些特定的情况下才可能会使用到 Union 索引合并.
- 情况一:二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只出现匹配部分列的情况。
- 情况二:主键列可以是范围匹配
- 情况三:使用 Intersection 索引合并的搜索条件
:::tips
查询条件符合了这些情况也不一定就会采用 Union 索引合并,也得看优化器的心情。
:::
Sort-Union合并
先按照二级索引记录的主键值进行排序,之后按照 Union 索引合并方式执行的方式称之为 Sort-Union 索引合并
11. 连接的原理
连接简介
连接的本质
连接 的本质就是把各个连接表中的记录都取出来依次匹配的组合加入结果集并返回给用户。
:::info
连接查询的结果集中包含一个表中的每一条记录与另一个表中的每一条记录相互匹配的组合,像这样的结果集就可以称之为 笛卡尔积
:::
连接过程简介
如果我们乐意,我们可以连接任意数量张表,但是如果没有任何限制条件的话,这些表连接起来产生的 笛卡尔积 可能是非常巨大的
所以在连接的时候过滤掉特定记录组合是有必要的,在连接查询中的过滤条件可以分成两种:
- 涉及单表的条件
- 涉及两表的条件
:::tips
例如:SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';
:::
在这个查询中我们指明了这三个过滤条件:
t1.m1 > 1
t1.m1 = t2.m2
-
查询过程:
首先确定第一个需要查询的表,这个表称之为 驱动表 。
- 针对上一步骤中从驱动表产生的结果集中的每一条记录,分别需要到 t2 表中查找匹配的记录,所谓 匹配的记录 ,指的是符合过滤条件的记录。
因为是根据 t1 表中的记录去找 t2 表中的记录,所以 t2 表也可以被称之为 被驱动表 。 :::info
- 当 t1.m1 = 2 时,过滤条件 t1.m1 = t2.m2 就相当于 t2.m2 = 2 ,所以此时 t2 表相当于有了 t2.m2 = 2 、 t2.n2 < ‘d’ 这两个过滤条件,然后到 t2 表中执行单表查询。
- 当 t1.m1 = 3 时,过滤条件 t1.m1 = t2.m2 就相当于 t2.m2 = 3 ,所以此时 t2 表相当于有了 t2.m2 = 3 、 t2.n2 < ‘d’ 这两个过滤条件,然后到 t2 表中执行单表查询。
::: :::danger 也就是说在两表连接查询中,驱动表只需要访问一次,被驱动表可能被访问多次。 :::
内连接和外连接
- 对于 内连接 的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集。
- 对于 外连接 的两个表,驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集。
:::info
ON 子句是专门为外连接驱动表中的记录在被驱动表找不到匹配记录时应不应该把该记录加入结果集这个场景下提出的,所以如果把 ON 子句放到内连接中, MySQL 会把它和 WHERE 子句一样对待,也就是说:内连接中的WHERE子句和ON子句是等价的。
:::
连接的原理
嵌套循环连接(Nested-Loop Join)
对于两表连接来说,驱动表只会被访问一遍,但被驱动表却要被访问到好多遍,具体访问几遍取决于对驱动表执行单表查询后的结果集中的记录条数。对于内连接来说,选取哪个表为驱动表都没关系,而外连接的驱动表是固定的。
这种驱动表只访问一次,但被驱动表却可能被多次访问,访问次数取决于对驱动表执行单表查询后的结果集中的记录条数的连接执行方式称之为 嵌套循环连接 ( Nested-Loop Join ),这是最简单,也是最笨拙的一种连接查询算法。
使用索引加快连接速度
查询 t2 表其实就相当于一次单表扫描,我们可以利用索引来加快查询速度。
- 在 m2 列上建立索引,因为对 m2 列的条件是等值查找
- 在 n2 列上建立索引,涉及到的条件是 t2.n2 < ‘d’ ,可能用到 range 的访问方法,
基于块的嵌套循环连接(Block Nested-Loop Join)
扫描一个表的过程其实是先把这个表从磁盘上加载到内存中,然后从内存中比较匹配条件是否满足。但是内存可能不够,被驱动表可是要被访问好多次的,如果这个被驱动表中的数据特别多而且不能使用索引进行访问,那就相当于要从磁盘上读好几次这个表,这个 I/O 代价就非常大了,所以我们得想办法:尽量减少访问被驱动表的次数。 :::tips 所以我们可不可以在把被驱动表的记录加载到内存的时候,一次性和多条驱动表中的记录做匹配,这样就可以大大减少重复从磁盘上加载被驱动表的代价了 :::12. MySQL基于成本的优化
什么是成本
其实在 MySQL 中一条查询语句的执行成本是由下边这两个方面组成的 :::tips
- I/O 成本:需要先把数据或者索引加载到内存中然后再操作
CPU 成本:读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为 CPU 成本。 :::
单表查询的成本
CREATE TABLE single_table (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
基于成本的优化步骤
:::info 在一条单表查询语句真正执行之前, MySQL 的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案,这个成本最低的方案就是所谓的 执行计划 :::
根据搜索条件,找出所有可能使用的索引
- 计算全表扫描的代价
- 计算使用不同索引执行查询的代价
- 对比各种执行方案的代价,找出成本最低的那一个
例如
SELECT * FROM single_table WHERE
key1 IN ('a', 'b', 'c') AND
key2 > 10 AND key2 < 1000 AND
key3 > key2 AND
key_part1 LIKE '%hello%' AND
common_field = '123';
1. 根据搜索条件,找出所有可能使用的索引
一个查询中可能使用到的索引称之为 possible keys 。 例如上边的查询语句可能用到的索引,也就是 possible keys 只有idx_key1
和 idx_key2
。
2. 计算全表扫描的代价
由于**查询成本= I/O 成本+ CPU 成本**
,所以计算全表扫描的代价需要两个信息:
:::tips
- 聚簇索引占用的页面数
- 该表中的记录数 ::: 这两个信息从哪来呢?设计 MySQL 的大叔为每个表维护了一系列的 统计信息 :
- Rows: 本选项表示表中的记录条数。
- Data_length: 本选项表示表占用的存储空间字节数。
用 InnoDB 存储引擎的表来说,该值就相当于聚簇索引占用的存储空间大小,也就是说可以这样计算该值的大小: **Data_length = 聚簇索引的页面数量 x 每个页面的大小 **
:::success
边查询结果显示 Data_length 的值是 1589248 ,所以我们可以反向来推导出 聚簇索引的页面数量 :
聚簇索引的页面数量 = 1589248 ÷ 16 ÷ 1024 = 97
:::
3. 计算使用不同索引查询的代价
我们需要分别分析单独使用这些索引执行查询的成本,最后还要分析是否可能使用到索引合并。这里需要提一点的是, MySQL 查询优化器先分析使用唯一二级索引的成本,再分析使用普通索引的成本,
查询的成本依赖两个方面的数据:
- 范围区间数量
- 需要回表的记录数
估回表操作的 I/O 成本依旧很豪放,认为每次回表操作都相当于访问一个页面,也就是说二级索引范围区间有多少记录,就需要进行多少次回表操作.
4. 对比各种执行方案的代价,找出成本最低的那一个
基于索引统计数据的成本计算
有时候使用索引执行查询时会有许多单点区间,比如使用 IN 语句就很容易产生非常多的单点区间,比如下边这个查询(下边查询语句中的 … 表示还有很多参数):SELECT * FROM single_table WHERE key1 IN ('aa1', 'aa2', 'aa3', ... , 'zzz');
如果按照之前的算法,需要一个个计算,当区间数比较多的时候会很不现实,因此MySQL的设计者定了一个系统变量,当单点区间超过一个阈值时,就用统计数据估算。
mysql> SHOW VARIABLES LIKE '%dive%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| eq_range_index_dive_limit | 200 |
+---------------------------+-------+
也就是说如果我们的 IN 语句中的参数个数小于200个的话,将使用 index dive 的方式计算各个单点区间对应的记录条数,如果大于或等于200个的话,可就不能使用 index dive 了,要使用所谓的索引统计数据来进行估算。
统计数据
:::info 像会为每个表维护一份统计数据一样, MySQL 也会为表中的每一个索引维护一份统计数据,查看某个表中索引的统计数据可以使用 SHOW INDEX FROM 表名 的语法 :::
- 使用 SHOW TABLE STATUS 展示出的 Rows 值,也就是一个表中有多少条记录。这个统计数据我们在前边唠叨全表扫描成本的时候说过很多遍了,就不赘述了。
- 使用 SHOW INDEX 语句展示出的 Cardinality 属性。结合上一个 Rows 统计数据,我们可以针对索引列,计算出平均一个值重复多少次。
:::tips
single_table 表的 idx_key1 索引为例,它的 Rows 值是 9693 ,它对应索引列 key1 的 Cardinality 值是968 ,所以我们可以计算key1 列平均单个值的重复次数就是:
9693 ÷ 968 ≈ 10(条)
对于之前的查询:SELECT * FROM single_table WHERE key1 IN ('aa1', 'aa2', 'aa3', ... , 'zzz');
假设 IN 语句中有20000个参数的话,就直接使用统计数据来估算这些参数需要单点区间对应的记录条数了,每个参数大约对应 10 条记录,回表的次数:20000 x 10 = 200000
::: 使用统计数据来计算单点区间对应的索引记录条数可比 index dive 的方式简单多了,但是它的致命弱点就是:不精确!
连接查询的成本
所以对于两表连接查询来说,它的查询成本由下边两个部分构成:
- 单次查询驱动表的成本
多次查询被驱动表的成本(具体查询多少次取决于对驱动表查询的结果集中有多少条记录)
扇出
我们把对驱动表进行查询后得到的记录条数称之为驱动表的 扇出 (英文名:
fanout
)。很显然驱动表的扇出值越小,对被驱动表的查询次数也就越少,连接查询的总成本也就越低。当查询优化器想计算整个连接查询所使用的成本时,就需要计算出驱动表的扇出值Condition filtering
很多情况下计算驱动表扇出值时需要靠 猜 :::info 这个 猜 的过程称之为 condition filtering 。当然,这个过程可能会使用到索引,也可能使用到统计数据,整个评估过程挺复杂的。 :::
两表连接的成本分析
:::tips
**连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本**
:::内连接和外连接
对于左(外)连接和右(外)连接查询来说,它们的驱动表是固定的,所以想要得到最优的查询方案只需要:分别为驱动表和被驱动表选择成本最低的访问方法。可是对于内连接来说,驱动表和被驱动表的位置是可以互换的,所以需要考虑两个方面的问题:不同的表作为驱动表最终的查询成本可能是不同的,也就是需要考虑最优的表连接顺序。然后分别为驱动表和被驱动表选择成本最低的访问方法。 :::info 计算内连接查询成本的方式更麻烦一些 ::: 查询优化器需要分别考虑这两种情况下的最优查询成本,然后选取那个成本更低的连接顺序以及该连接顺序下各个表的最优访问方法作为最终的查询计划。
优化的重点
所以我们的优化重点其实是下边这两个部分:
尽量减少驱动表的扇出
- 对被驱动表的访问成本尽量低
14. MySQL基于规则的优化
条件化简
:::warning 我们编写的查询语句的搜索条件本质上是一个表达式,这些表达式可能比较繁杂,或者不能高效的执行, MySQL的查询优化器会为我们简化这些表达式。 :::
移除不必要的括号
((a = 5 AND b = c) OR ((a > c) AND (c < 5)))
--> (a = 5 and b = c) OR (a > c AND c < 5)
常量传递(constant_propagation)
a = 5 AND b > a
-->
a = 5 AND b > 5
等值传递(equality_propagation)
- 移除没用的条件(trivial_condition_removal)
- 表达式计算
- HAVING子句和WHERE子句的合并
- 常量表检测
外连接消除
:::success 内连接 的驱动表和被驱动表的位置可以相互转换,而 左(外)连接 和 右(外)连接 的驱动表和被驱动表是固定的。这就导致 内连接 可能通过优化表的连接顺序来降低整体的查询成本,而 外连接 却无法优化表的连接顺序。 ::: 外连接和内连接的本质区别就是:
- 对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充;
- 而内连接的驱动表的记录如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录会被舍弃。
我们知道 WHERE 子句的杀伤力比较大,凡是不符合WHERE子句中条件的记录都不会参与连接。只要我们在搜索条件中指定关于被驱动表相关列的值不为 NULL ,那么外连接中在被驱动表中找不到符合 ON 子句条件的驱动表记录也就被排除出最后的结果集了,也就是说:在这种情况下:外连接和内连接也就没有什么区别了!
mysql> SELECT * FROM t1 LEFT JOIN t2 ON t1.m1 = t2.m2 WHERE t2.n2 IS NOT NULL;
:::info 我们把这种在外连接查询中,指定的 WHERE 子句中包含被驱动表中的列不为 NULL 值的条件称之为 空值拒绝(英文名: reject-NULL )。 :::
子查询优化
18. InnoDB的Buffer Pool
缓存的重要性
:::warning 需要缓存的原因是在于计算机对于内存的读写速度很快,对于磁盘的读写速度很慢。 ::: InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其 缓存 起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。
InnoDB的Buffer Pool
:::warning 为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,这片内存叫做 Buffer Pool (中文名是 缓冲池 ) :::
Buffer Pool内部组成
- Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB 。
- 为了更好的管理这些在Buffer Pool 中的缓存页,缓存页都创建了一些所谓的 控制信息 ,这些控制信息包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息。
每个缓存页对应的控制信息占用的内存大小是相同的,每个页对应的控制信息占用的一块内存称为一个控制块,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool的前边,缓存页被存放到 Buffer Pool 后边
每个控制块大约占用缓存页大小的5%,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小
free链表的管理
如何区分哪些缓存页已经使用了,哪些还没用呢?
:::warning
我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作 free链表 (或者说空闲链表)。
:::
缓存页的哈希处理
我们怎么知道该页在不在 Buffer Pool 中呢?
:::success
可以用 **表空间号 + 页号**
作为 **key**
, **缓存页**
作为 **value**
创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据 表空间号 + 页号 看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从 free链表 中选一个空闲的缓存页
:::
flush链表的管理
脏页
当修改了Buffer Pool中某一页的内容时,内存中的数据和磁盘中的数据变得不一样了,理想的方法是修改了内存中的数据就立刻将这数据也在磁盘中修改,但是这样会耗时较高,因此会将此页标记为脏页,并且先不修改磁盘的内容。
flush链表
如何确定哪些页是脏页?
:::warning
创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush链表 。
:::
LRU链表
Buffer Pool的大小是有限的 :::success 对于一些频繁访问的数据我们希望可以一直留在 Buffer Pool 中,而一些很少访问的数据希望可以在某些时机可以淘汰掉,从而保证 Buffer Pool 不会因为满了而导致无法再缓存新的数据,同时还能保证常用数据留在 Buffer Pool 中。要实现这个,最容易想到的就是 LRU(Least recently used)算法。 ::: 简单的 LRU 算法的实现思路是这样的:
- 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部。
当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。
划分区域的LRU链表
在实际情况下,LRU链表会遇到两种情况导致其工作效率下降
预读失效;
- Buffer Pool 污染;
预读
程序是有空间局部性的,靠近当前被访问数据的数据,在未来很大概率会被访问到。所以,MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。但是可能这些被提前加载进来的数据页,并没有被访问,相当于这个预读是白做了,这个就是预读失效。
预读可以分为:
线性预读 :::info 如果顺序访问了某个区( extent )的页面超过这个系统变量的值,就会触发一次 异步 读取下一个区中全部的页面到 Buffer Pool 的请求,注意 异步 读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。 :::
随机预读 :::info 如果 Buffer Pool 中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次 异步 读取本区中所有其的页面到 Buffer Pool 的请求。 :::
Buffer Pool污染
当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。比如全表扫描。
改进方案
MySQL 是这样做的,它改进了 LRU 算法,将 LRU 划分了 2 个区域:old 区域 和 young 区域。
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做 热数据 ,或者称 young区域 。
- 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做 冷数据 ,或者称 old区域 。
young 区域在 LRU 链表的前半部分,old 区域则是在后半部分
我们是按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,某些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。那这个划分成两截的比例怎么确定呢?对于 InnoDB 存储引擎来说,我们可以通过查看系统变量 innodb_old_blocks_pct 的值来确定 old 区域在 LRU链表 中所占的比例
- 划分这两个区域后,预读的页就只需要加入到 old 区域的头部,当页被真正访问的时候,才将页插入 young 区域的头部。如果预读的页一直没有被访问,就会从 old 区域移除,这样就不会影响 young 区域中的热点数据
- 像前面这种全表扫描的查询,很多缓冲页其实只会被访问一次,但是它却只因为被访问了一次而进入到 young 区域,从而导致热点数据被替换了。LRU 链表中 young 区域就是热点数据,只要我们提高进入到 young 区域的门槛,就能有效地保证 young 区域里的热点数据不会被替换掉。
- 具体是这样做的,在对某个处在 old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:
- Free Page(空闲页),表示此页未被使用,位于 Free 链表;
- Clean Page(干净页),表示此页已被使用,但是页面未发生修改,位于LRU 链表。
- Dirty Page(脏页),表示此页「已被使用」且「已经被修改」,其数据和磁盘上的数据已经不一致。当脏页上的数据写入磁盘后,内存数据和磁盘数据一致,那么该页就变成了干净页。脏页同时存在于 LRU 链表和 Flush 链表。
刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。
- 从 LRU链表 的冷数据中刷新一部分页面到磁盘。
- 从 flush链表 中刷新一部分页面到磁盘。
下面几种情况会触发脏页的刷新:
- 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
- Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
- MySQL 认为空闲时,后台线程回定期将适量的脏页刷入到磁盘;
- MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;
多个Buffer Pool实例
在 Buffer Pool 特别大的时候,我们可以把它们拆分成若干个小的 Buffer Pool ,每个 Buffer Pool 都称为一个 实例 ,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力chunk
我们要重新调整 Buffer Pool 大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的 Buffer Pool 中的内容复制到这一
块新空间,这是极其耗时的。所以设计 MySQL 的大叔们决定不再一次性为某个 Buffer Pool 实例向操作系统申请一大片连续的内存空间,而是以一个所谓的chunk
为单位向操作系统申请空间
:::warning 一个 Buffer Pool 实例其实是由若干个 chunk 组成的,一个 chunk 就代表一片连续的内存空间 ::: 我们在服务器运行期间调整 Buffer Pool 的大小时就是以 chunk 为单位增加或者删除内存空间,而不需要重新向操作系统申请一片大的内存,然后进行缓存页的复制。不过需要注意的是,**innodb_buffer_pool_chunk_size**
的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的。