《MySQL是怎么运行的》阅读分享

mysql运行的整体架构简介

《MySQL是怎么运行的》阅读分享_第1张图片

Mysql是由两部分构成,一部分是服务器程序,一部分是客户端程序。
服务器程序又包括两部分:
第一部分server层包括连接器、查询缓存、分析器、优化器、执行器等。涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等
第二部分是存储引擎层负责数据的存储和提取。存储引擎有多种选择,主要有InnoDB、MyISAM、Memory等。

要操作Mysql数据库,首先客户端要连接上mysql服务器程序。
连接器: 负责跟客户端建立连接、获取权限、维持和管理连接。
连接命令:

mysql -h$ip -P$port -u$user -p

MySQL采用的TCP/IP协议进行网络通信,客户端和服务端之间通过三次握手建立连接。

连接上数据库后,就可以执行sql语句了(以查询语句为例)。

sql查询语句命中缓存
查询缓存: 当sql是查询语句,MySQL 拿到这个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。这个查询请求能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。查询缓存前要校验用户对表是否有查询权限。

查询缓存往往弊大于利:
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空,对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
通过设置参数 query_cache_type,选择是否使用查询缓存。
mysql8.0已经将查询缓存的整块功能删掉了。

没有命中查询缓存,就要开始真正执行语句了
分析器: 分析器会做“词法分析”和“语法分析”以及“语义分析等,判断sql语句中的关键字,表,语法是否正确。

如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”,可以很方便检查sql语句。

优化器: 语法解析之后,服务器程序获得到了需要的信息,比如要查询的列是哪些,表是哪个,搜索条件是什么等等。但光有这些是不够的,因为我们写的MySQL语句执行起来效率可能并不是很高,MySQL的优化程序会对我们的语句做一些优化,如外连接转换为内连接、表达式简化、子查询转为连接等。可以通过expllian语句来查看某个sql语句的执行计划。

执行器: MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。先是校验权限,要先判断一下你对这个表 T 有没有执行查询的权限,然后根据表的引擎定义,去使用这个引擎提供的接口。

存储引擎: 储存数据,并提供读写接口。

存储引擎的一些操作:

查看当前服务器程序支持的存储引擎:

SHOW ENGINES;

设置表的存储引擎

-- 创建表时指定存储引擎
CREATE TABLE 表名(
    建表语句;
) ENGINE = 存储引擎名称;

-- 修改表的存储引擎
ALTER TABLE 表名 ENGINE = 存储引擎名称;

查看表使用的存储引擎

 SHOW CREATE TABLE  表名

字符集和比较规则

字符集:表示字符的范围以及编码规则,字符编码规则是指一种映射规则,根据这个映射规则可以将某个字符映射成其他形式的数据以便在计算机中存储和传输。例如ASCII字符编码规定使用单字节中低位的7个比特去编码所有的字符,在这个编码规则下字母A的编号是65(ASCII码),用单字节表示就是0x41,因此写入存储设备的时候就是二进制的 01000001。
以下是ASCLL字符编码规则以及字符范围(128个)。

一些重要的字符集
GB2312字符集

 收录了汉字以及拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母。
 其中收录汉字6763个,其他文字符号682个。
 同时这种字符集又兼容ASCII字符集,所以在编码方式上显得有些奇怪:

-如果该字符在ASCII字符集中,则采用1字节编码。否则采用2字节编码。

GBK字符集

GBK字符集只是在收录字符范围上对GB2312字符集作了扩充,编码方式上兼容GB2312。

utf8字符集

收录地球上能想到的所有字符,而且还在不断扩充。
这种字符集兼容ASCII字符集,采用变长编码方式,编码一个字符需要使用1~4个字节

MySQL中支持的字符集
查看当前MySQL中支持的字符集

SHOW CHARSET;

MySQL中的utf8和utf8mb4区别

utf8mb3:阉割过的utf8字符集,只使用1~3个字节表示字符。
utf8mb4(mysql 5.5.3版本之后):正宗的utf8字符集,使用1~4个字节表示字符。

某些中文生僻字或者emoji表情,是四个字符的,只能使用utf8mb4编码

字符集的比较规则:既比较两个字符大小的规则,每种字符集对应若干种比较规则,每种字符集都有一种默认的比较规则。例如:不区分大小写,按照中文拼音顺序等。

查看MySQL中支持的比较规则的命令

SHOW COLLATION 

MySQL有4个级别的字符集和比较规则

  • 服务器级别
  • 数据库级别
  • 表级别
  • 列级别

服务器级别

-- 查看Mysql服务器的字符集
SHOW VARIABLES LIKE 'character_set_server';

-- 查看Mysql服务器的比较规则
SHOW VARIABLES LIKE 'collation_server';

-- 修复服务器的字符集和比较规则
-- 可以在启动服务器程序时通过启动选项
-- 或者在服务器程序运行过程中使用SET语句修改这两个变量的值。
[server]
character_set_server=gbk
collation_server=gbk_chinese_ci

数据库级别

-- 查看数据库的字符集
 SHOW VARIABLES LIKE 'character_set_database';

-- 查看数据库的比较规则
SHOW VARIABLES LIKE 'collation_database';

-- 创建数据库时指定字符集和比较规则
CREATE DATABASE 数据库名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称];

-- 修改数据库指定字符集和比较规则
ALTER DATABASE 数据库名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称];

表级别

-- 查看表的字符集
show create table <表名>;

-- 查看表的比较规则
show table status from 数据库名 like '%表名%‘ ;

-- 创建表时指定字符集和比较规则
CREATE TABLE 表名 (列的信息)
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称]]

-- 修改表指定字符集和比较规则
ALTER TABLE 表名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称]

列级别

-- 查看列的字符集和比较规则
select *  
FROM information_schema.`COLUMNS`
where TABLE_SCHEMA = '表名'

-- 创建表时指定列的字符集和比较规则
CREATE TABLE 表名(
    列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
    其他列...
);

-- 修改列指定字符集和比较规则
ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];

在转换列的字符集时需要注意,如果转换前列中存储的数据不能用转换后的字符集进行表示,就会发生错误。比方说原先列使用的字符集是utf8,列中存储了一些汉字,现在把列的字符集转换为ascii的话就会出错,因为ascii字符集并不能表示汉字字符。

创建时规则:

  • 如果创建或修改列时,没有显式的指定字符集和比较规则,则该列默认用表的字符集和比较规则
  • 如果创建或修改表时,没有显式的指定字符集和比较规则,则该表默认用数据库的字符集和比较规则
  • 如果创建或修改数据库时,没有显式的指定字符集和比较规则,则该数据库默认用服务器的字符集和比较规则

修改时规则:

  • 只修改字符集,则比较规则将变为修改后的字符集默认的比较规则。
  • 修改比较规则,则字符集将变为修改后的比较规则对应的字符集。

客户端和服务器通信中的字符集

《MySQL是怎么运行的》阅读分享_第2张图片

  • 客户端使用操作系统的字符集编码请求字符串,向服务器发送的是经过编码的一个字节串。

  • 服务器将客户端发送来的字节串采用character_set_client代表的字符集进行解码,将解码后的字符串再按照character_set_connection代表的字符集进行编码。

  • 如果character_set_connection代表的字符集和具体操作的列使用的字符集一致,则直接进行相应操作,否则的话需要将请求中的字符串从character_set_connection代表的字符集转换为具体操作的列使用的字符集之后再进行操作。

  • 将从某个列获取到的字节串从该列使用的字符集转换为character_set_results代表的字符集后发送到客户端。

  • 客户端使用操作系统的字符集解析收到的结果集字节串。

我们通常都把 character_set_client 、character_set_connection*、character_set_results*** 这三个系统变量设置成和客户端使用的字符集一致的情况,这样减少了很多无谓的字符集转换
相关sql

-- 查看字符集
show variables like 'character_set_%';
-- 设置字符集
set character_set_client = 字符集名;
set character_set_connection =  字符集名;
set character_set_results =  字符集名;

InnoDB记录结构

Mysql默认使用InnoDB作为存储引擎的数据存储结构,我们最常用到的存储引擎也是Innodb,故要了解的是使用InnoDB作为存储引擎的数据存储结构。

数据页简介
InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
后面介绍——行记录、页结构、区概念、段概念、独立表空间和系统表空间

行记录

在mysql中,行记录是数据存储的基本单位,我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。InnoDB存储引擎有四种行格式,分别是Compact(紧凑的)、Redundant(冗余的)、Dynamic(动态的)和Compressed(压缩的)。虽有不同,但原理相同。

创建或修改表的语句中指定行格式

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
    
ALTER TABLE 表名 ROW_FORMAT=行格式名称

查看当前表指定的行格式

show table status from lottery like '%表名%' ;

Compact(紧凑的)行格式

《MySQL是怎么运行的》阅读分享_第3张图片
一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分

记录的额外信息
变长字段长度列表
存放所有变长字段的真实数据占用的字节长度,每个可变长字段的对应的长度按照列的顺序逆序存放;
变长字段中存储多少字节的数据是不固定的,故需要记录变长字段的真实数据占用的字节长度。
变长字段类型包括VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型等。
NULL值列表
用于标识表中允许存储NULL的列,是否为空。也是按照列的顺序逆序排列
记录头信息
由固定的5个字节组成
《MySQL是怎么运行的》阅读分享_第4张图片
《MySQL是怎么运行的》阅读分享_第5张图片
记录的真实数据
除了用户自己定义的列的数据以外,MySQL会为每个记录默认的添加一些列,
row_id (唯一标识一条记录,占6字节,当表中有主键或唯一约束,无改字段)、transaction_id(事务ID,占6字节)、roll_pointer(回滚指针,占7字节)

数据演示:
准备表和表数据:

-- 创建表
CREATE TABLE record_format_demo (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10),
c4 VARCHAR(10)
) CHARSET=ascii ROW_FORMAT=COMPACT;
-- 插入表数据
INSERT INTO record_format_demo(c1, c2, c3, c4) 
values
('aaaa', 'bbb', 'cc', 'd'), 
('eeee', 'fff', NULL, NULL);

当前表结构:
在这里插入图片描述
表record_format_demo使用的字符集是ascii,行格式是compact,可以得到表中两条记录的存储格式详情如下:
《MySQL是怎么运行的》阅读分享_第6张图片
Compact(紧凑的)行格式对CHAR(M) 类型的处理
对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。

Redundant行格式

《MySQL是怎么运行的》阅读分享_第7张图片
与Compact(紧凑的)行格式相比,记录字段的位置,通过字段长度偏移列表。字段长度偏移指的是从第一列的真实数据的开始的到当前列的真实数据的结尾。

Redundant的记录头信息
《MySQL是怎么运行的》阅读分享_第8张图片
与Compact(紧凑的)行格式的记录头信息相比
Redundant (冗余的)行格式多了 n_field 和 1byte_offs_flag 这两个属性。
Redundant 冗余的)行格式没有 record_type 这个属性。
当表record_format_demo使用的字符集是ascii,行格式是Redundant 冗余的),可以得到表中两条记录的存储格式详情如下:
《MySQL是怎么运行的》阅读分享_第9张图片

Redundant行格式对CHAR(M) 类型的处理
Redundant行格式不管该列使用的字符集是什么,只要是使用CHAR(M)类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和M的乘积。比方说使用utf8字符集的CHAR(10)类型的列占用的真实数据空间始终为30个字节,使用gbk字符集的CHAR(10)类型的列占用的真实数据空间始终为20个字节。

行溢出数据
对于VARCHAR(M)类型的列最多可以占用65535个字节,其中的M代表该类型最多存储的字符数量。这个65535个字节除了列本身的数据之外,还包括一些其他的数据(storage overhead),比如说以Compact(紧凑的)行格式为例,我们为了存储一个VARCHAR(M)类型的列,其实需要占用3部分存储空间:

  • 真实数据
  • 真实数据占用字节的长度
  • NULL值标识(该列没有NOT NULL属性)

除去真实数据占用字节的长度占的两字节,NULL值标识标识占的一字节,真实数据还可使用65532字节。
utf8mb4字符集表示一个字符最多需要4个字节,那在该字符集下,M的最大取值就是16,383,就是说最多能存储16,383(也就是:65532/4)个字符。

行溢出处理
MySQL中磁盘和内存交互的基本单位是页,以页为基本单位来管理存储空间的,我们的记录都会被分配到某个页中存储。一个页的大小是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65532个字节,会出现一个页存放不了一条记录情况。
对于Compact和Reduntant行格式来说,如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前768个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页。
《MySQL是怎么运行的》阅读分享_第10张图片

Dynamic和Compressed行格式

Dynamic和Compressed行格式,这俩行格式和Compact行格式类似,在处理行溢出数据时有不同,它们不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
Compressed行格式和Dynamic不同的一点是,Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。
《MySQL是怎么运行的》阅读分享_第11张图片

InnoDB数据页结构

页是Innodb管理存储空间的基本单位,大小一般是16KB,InnoDB为了不同目的,有许多不同类型的页,比如存放表空间头部信息的页,存放Insert Buffer信息的页,存放INODE信息的页,存放undo日志信息的页等。我们聚焦的是那些存放我们表中记录的那种类型的页,称为数据页(索引(INDEX)页)。

数据页整体结构

《MySQL是怎么运行的》阅读分享_第12张图片

数据页中的行记录(用户记录以及最大、最小记录)

行记录的格式已经了解了,现在重点看行记录中的记录头信息。
还是以Compact(紧凑的)行格式为例:
《MySQL是怎么运行的》阅读分享_第13张图片
《MySQL是怎么运行的》阅读分享_第14张图片
准备演示数据:

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

INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd');

《MySQL是怎么运行的》阅读分享_第15张图片

  • delete_mask
    标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了。
    这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间(介绍事务的时候会详细介绍删除操作的详细过程)
  • min_rec_mask
    B+树的每层非叶子节点中的最小记录都会添加该标记,我们插入的四条记录的min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录。
  • n_owned
    当前记录拥有的记录数。
  • heap_no
    示当前记录在本页中的位置
  • record_type
    表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录。
  • next_record
    表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。

页中记录按照主键从小到大的顺序形成了一个单链表,通过next_record作为引用找到下一个节点记录,页中维护了两个初始节点记录,Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录),record_type区分用户记录(0或1)、最小记录(2)和最大记录(3)。min_rec_mask只有B+树的每层非叶子节点中的最小记录是1,其他记录都是0。heap_no标识记录位置,最小记录为0,依次是用户记录递增,最后是最大记录。

《MySQL是怎么运行的》阅读分享_第16张图片

数据页中的Free Space(空闲空间)

就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了。

数据页中的Page Directory(页目录)

Page Directory(页目录)为了快速在页中查找某条记录。
页目录构建规则:

  • 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。

  • 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。

  • 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
    《MySQL是怎么运行的》阅读分享_第17张图片
    页中查找页的过程

  • 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。

  • 通过记录的next_record属性遍历该槽所在的组中的各个记录。

数据页中的Page Header(页面头部)

是专门针对数据页记录的各种状态信息,记录的信息包括本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,,这个部分占用固定的56个字节。
《MySQL是怎么运行的》阅读分享_第18张图片

  • PAGE_DIRECTION
    用来表示最后一条记录插入方向的状态,假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。

  • PAGE_N_DIRECTION
    表示最后插入记录的方向的连续数量,最后插入方向与上一次插入方向不同,清零重新计算。

数据页中的File Header(文件头部)

File Header针对各种类型的页都通用,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁等, 这个部分占用固定的38个字节。
《MySQL是怎么运行的》阅读分享_第19张图片

  • FIL_PAGE_SPACE_OR_CHKSUM
    这个代表当前页面的校验和(checksum),快速比较页是否相同。
  • FIL_PAGE_OFFSET
    每一个页都有一个单独的页号,InnoDB通过页号来可以唯一定位一个页。
    -FIL_PAGE_TYPE
    上面介绍的其实都是存储记录的数据页,其实还有很多别的类型的页
    《MySQL是怎么运行的》阅读分享_第20张图片
  • FIL_PAGE_PREV和FIL_PAGE_NEXT
    FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。所有的数据页其实是一个双链表。
    《MySQL是怎么运行的》阅读分享_第21张图片

数据页中的File Trailer(文件尾部)

可以分成2个小部分:

  • 前4个字节代表页的校验和
    这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,File Header在页面的前面,校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
    这个部分也是为了校验页的完整性的。

B+树索引

InnoDB中的索引方案

《MySQL是怎么运行的》阅读分享_第22张图片
为了更方便查找记录,我们把数据页存放到B+树这个数据结构中的最底层的节点上,这些节点也被称为叶子节点或叶节点。B+树的非叶子节点是都是目录页。
目录项记录和普通的用户记录的不同点:

  • 目录项记录的record_type值是1,而普通用户记录的record_type值是0。

  • 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。

  • 记录头信息的min_rec_mask的属性,只有在存储目录项记录的页中的主键值最小的目录项记录的min_rec_mask值为1,其他别的记录的min_rec_mask值都是0。

select * from 表 where 主键 = 1;
查找过程:
1.如果B+树只有一层,也就是只有根节点,该节点也就是数据页,查找过程:

  • 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
  • 通过记录的next_record属性遍历该槽所在的组中的各个记录。
    2.如果B+树有两层及以上,只有最底层的节点类型是数据页,其他层的节点类型查找过程:
  • 从根节点出发,通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
    递归查找下一层节点(所在的槽分组中的节点数最多8条)。
  • 当到达最底层,找到数据页节点,还是通过二分查找定该记录所在的槽,再通过记录的next_record属性遍历该槽所在的组中的各个记录。
    设B+树为节点上的记录数为m,一共有n个节点。
    访问的节点数量 ≈ \approx B+树深度 = l o g m n logm^n logmn
    每个节点访问时间复杂度=在槽中二分查找下一层节点时间复杂度
    ≈ \approx O( l o g 2 m log2^m log2m
    )
    可知
    B+树中查找一个记录时间复杂度=访问的节点数量×每个节点访问时间复杂度= l o g m n logm^n logmn×O( l o g 2 m log2^m log2m
    )
    B+树的搜索过程中的IO次数 = 搜索过程中访问节点的数量 ≈ \approx B+树的深度 = l o g m n logm^n logmn

B+树都不会超过4层,数据页中用户记录最多存放100条记录,目录页中目录记录最多存放1000条,如果B+树是4层,也就是100×1000×1000×1000=100000000000,既一千亿条数据。

B+树索引的分类

聚簇索引

所有完整的用户记录都存放在这个聚簇索引的叶子节点处,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据,数据即索引。
它有两个特点:
1.使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:

  • 页内的记录是按照主键的大小顺序排成一个单向链表。

  • 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。

  • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。

2.B+树的叶子节点存储的是完整的用户记录。

所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

非聚簇索引(二级索引)

在非聚簇索引的叶子节点上存储的并不是真正的行数据,而是主键 +当前索引列。
按字段特性分类可分为:唯一索引、普通索引、前缀索引。

  • 唯一索引
    建立在UNIQUE字段上的索引被称为唯一索引,一张表可以有多个唯一索引,索引列值允许为空,列值中出现多个空值不会发生重复冲突。

  • 普通索引
    建立在普通字段上的索引被称为普通索引。

  • 前缀索引
    前缀索引是指对字符类型字段的前几个字符或对二进制类型字段的前几个bytes建立的索引,而不是在整个字段上建索引。前缀索引可以建立在类型为char、varchar、binary、varbinary的列上,可以大大减少索引占用的存储空间,也能提升索引的查询效率。

按字段个数分类可分为:单列索引、联合索引(复合索引、组合索引)。

  • 单列索引
    建立在单个列上的索引被称为单列索引。

  • 联合索引(复合索引、组合索引)
    建立在多个列上的索引被称为联合索引,又叫复合索引、组合索引。
    回表
    在非聚簇索引中查找到的最终结果是——主键 +当前索引列,当前索引列可能无法包含select的数据列(select的数据列能直接从二级索引中取得,称为覆盖索引)还需拿着主键去聚簇索引中再进行查询。聚簇索引中才包含

B+树索引的使用

索引的代价

索引是个好东西,可不能乱建。一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。

  • 空间上的代价
    每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,一棵很大的B+树由许多数据页组成,是很大的一片存储空间
  • 时间上的代价
    每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。

B+树索引适用的情况

准备数据:

CREATE TABLE person_info(
    id INT NOT NULL auto_increment,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday_phone_number (name, birthday, phone_number)
);

该表拥有主键索引 key(id)和组合索引idx_name_birthday_phone_number (name, birthday, phone_number)

全值匹配

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';

组合索引idx_name_birthday_phone_number (name, birthday, phone_number),按照name,birthday, phone_number顺序排序,查询条件中三个字段都是等值比较。索条件中的列和索引列一致的话,这种情况为全值匹配。

匹配左边的列

SELECT * FROM person_info WHERE name = 'Ashburn';

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';

搜索语句中也可以不用包含全部联合索引中的列,只包含左边的就行。

匹配列前缀

SELECT * FROM person_info WHERE name LIKE 'As%';

B+树中的数据页和记录通过该列的字符集和比较规则进行排序的,这些字符串的前n个字符,也就是前缀都是排好序的,所以对于字符串类型的索引列来说,我们只匹配它的前缀也是可以快速定位记录的。
匹配范围值

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';
-- 如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';

由于B+树中的数据页和记录是先按name列排序的,name列相同再按birthday列排序,birthday列相同再按照phone_number列排序。

精确匹配某一列并范围匹配另外一列

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';

对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找

用于排序

SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;

因为这个B+树索引本身就是按照上述规则排好序的,所以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了。

用于分组

SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number

和使用B+树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组
在使用索引时需要注意下面这些事项:

  • 只为用于搜索、排序或分组的列创建索引
  • 为列的基数大(某列不重复的个数)的列创建索引
  • 索引列的类型尽量小
  • 可以只对字符串值的前缀建立索引
  • 只有索引列在比较表达式中单独出现才可以适用索引
  • 为了尽可能少的让聚簇索引发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT属性。
  • 定位并删除表中的重复和冗余索引
  • 尽量使用覆盖索引进行查询,避免回表带来的性能损耗。

InnoDB的表空间

区的概念——连续的64个页就是一个区。
不管独立系统表空间还是独立表空间,都可以看成是由若干个区组成的。
每256个区又分一组。
**引入区的原因:**进行范围查找的时候,利用B+树直接定位到最左边记录和最右边记录,然后沿着页之间的双向链表,页内行之间的单向链表一直扫描,如果页之前的距离非常远,就会有随机I/O,这是非常慢的。区在物理位置上是连续的64页,这样在同一个区中查找就是顺序I/O,是非常快的。表中数据非常多时,甚至一次性分配多个物理位置上连续的区。
《MySQL是怎么运行的》阅读分享_第23张图片
段的概念——段不对应表空间中某一个连续的物理空间,而是一个逻辑上的概念,由若干个零散的页以及一些完整的区组成。
考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,InnoDB提出了一个碎片(fragment)区的概念。也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。所以此后为某个段分配存储空间的策略是这样的:

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

引入段的原因: 范围查找时,不区分B+树叶子节点和非叶子节点,都放在同一个区中,范围扫描效果还是不行的,叶子节点和非叶子节点可以交错存放在一个区中,还是会导致随机I/O。一个段对应“一个索引叶子节点的区的集合”或者“非叶子节点的区的集合”。故一个索引对应两个段。
区的分类

  • 空闲的区:现在还没有用到这个区中的任何页。

  • 有剩余空间的碎片区:表示碎片区中还有可用的页。

  • 没有剩余空间的碎片区:表示碎片区中的所有页都被使用,没有空闲页。

  • 附属于某个段的区。每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。

独立表空间结构和系统表空间

表空间结构整体图:

《MySQL是怎么运行的》阅读分享_第24张图片
第一个组最开始的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这些区的属性,对于在extent 512区中的该类型页存储的就是extent 512 ~ extent 767这些区的属性。上面介绍的FSP_HDR类型的页其实和XDES类型的页的作用类似,只不过FSP_HDR类型的页还会额外存储一些表空间的属性。

  • IBUF_BITMAP类型:同上。

XDES Entry的结构
为了方便管理这些区,设计InnoDB的大佬设计了一个称为XDES Entry的结构(全称就是Extent Descriptor Entry),每一个区都对应着一个XDES Entry结构,这个结构记录了对应的区的一些属性。

《MySQL是怎么运行的》阅读分享_第25张图片

  • Segment ID(8字节)
    该区所在的段Id
  • List Node(12字节)
    Pre Node Page Number和Pre Node Offset的组合就是指向前一个XDES Entry的指针
    Next Node Page Number和Next Node Offset的组合就是指向后一个XDES Entry的指针。
  • State(4字节)
    表明区的状态。FREE(空闲的区)、FREE_FRAG(有剩余空间的碎片区)、FULL_FRAG(没有剩余空间的碎片区)和FSEG(附属某个段的区)。
  • Page State Bitmap(16字节)
    共占用16个字节,也就是128个比特位,区中一共有64个页,每两个比特对应一个页,标识页是否空闲。

XDES Entry链表

为了快速定位未使用的页用来插入数据。InnoDB给每个段中的区对应的XDES Entry结构建立了三个链表,通过List Node作为指针(这三个链表上的区都是直属某个段,既区的类型是FSEG(附属某个段的区)):

  • FREE链表:同一个段中,所有页都是空闲的区对应的XDES Entry结构会被加入到这个链表。注意和直属于表空间的FREE链表区别开了,此处的FREE链表是附属于某个段的。
  • NOT_FULL链表:同一个段中,仍有空闲空间的区对应的XDES Entry结构会被加入到这个链表。
  • FULL链表:同一个段中,已经没有空闲空间的区对应的XDES Entry结构会被加入到这个链表。

段中数据较少的时候,首先会查看表空间中是否有状态为FREE_FRAG的区,也就是找还有空闲空间的碎片区,如果找到了,那么从该区中取一些零碎的页把数据插进去;否则到表空间下申请一个状态为FREE的区,也就是空闲的区,把该区的状态变为FREE_FRAG,然后从该新申请的区中取一些零碎的页把数据插进去。
只要拿到这三个链表的基节点(头节点),也就可以拿到这三种状态的区。

链表基节点
《MySQL是怎么运行的》阅读分享_第26张图片

  • List Length表明该链表一共有多少节点,

  • First Node Page Number和First Node Offset表明该链表的头节点在表空间中的位置。

  • Last Node Page Number和Last Node Offset表明该链表的尾节点在表空间中的位置。

INODE Entry结构

每个段都定义了一个INODE Entry结构来记录一下段中的属性。
《MySQL是怎么运行的》阅读分享_第27张图片

  • 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。so easy!
  • Magic Number:
      这个值是用来标记这个INODE Entry是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。
  • Fragment Array Entry
      我们前面强调过无数次:段是一些零散页和一些完整的区的集合,每个Fragment Array Entry结构都对应着一个零散的页,这个结构一共4个字节,表示一个零散页的页号。

各类型页详细情况

FSP_HDR类型

FSP_HDR页,是第一个组的第一个页,也是表空间的第一个页

《MySQL是怎么运行的》阅读分享_第28张图片
《MySQL是怎么运行的》阅读分享_第29张图片
重点来看看File Space Header和XDES Entry这两个部分(其它部分在页结构都介绍过);
File Space Header部分
《MySQL是怎么运行的》阅读分享_第30张图片

  • List Base Node for FREE List、List Base Node for FREE_FRAG List、List Base Node for FULL_FRAG List。
    分别是直属于表空间的FREE链表的基节点、FREE_FRAG链表的基节点、FULL_FRAG链表的基节点,这三个链表的基节点在表空间的位置是固定的,就是在表空间的第一个页(也就是FSP_HDR类型的页)的File Space Header部分
  • FRAG_N_USED
    表示FREE_FRAG链表中已经使用的页数量,方便之后在链表中查找空闲的页。
  • FREE Limit
    在该字段表示的页号之前的区都被初始化了,之后的区尚未被初始化
    表空间都对应着具体的磁盘文件,一开始我们创建表空间的时候对应的磁盘文件中都没有数据,所以我们需要对表空间完成一个初始化操作,包括为表空间中的区建立XDES Entry结构,为各个段建立INODE Entry结构,建立各种链表等等的各种操作。一开始就为表空间申请一个特别大的空间,但是实际上有绝大部分的区是空闲的,我们可以选择把所有的这些空闲区对应的XDES Entry结构加入FREE链表,也可以选择只把一部分的空闲区加入FREE链表
  • Next Unused Segment ID
    该字段表明当前表空间中最大的段ID的下一个ID,方便创建新段的时候赋予新段一个唯一的ID值
  • Space Flags
    略。
  • List Base Node for SEG_INODES_FULL List和List Base Node for SEG_INODES_FREE List
    代表两个基结点
    SEG_INODES_FULL链表,该链表中的INODE类型的页都已经被INODE Entry结构填充满了,没空闲空间存放额外的INODE Entry了。SEG_INODES_FREE链表,该链表中的INODE类型的页都已经仍有空闲空间来存放INODE Entry结构。

XDES Entry部分
256个区划分成一组,在每组的第一个页中存放256个XDES Entry结构,每个XDES Entry记录了对应的区的一些属性。结构已经介绍了。
《MySQL是怎么运行的》阅读分享_第31张图片

IBUF_BITMAP类型

这种类型的页里边记录了一些有关Change Buffer,后面再详细看。

INODE类型

INODE类型的页就是为了存储INODE Entry结构而存在的。INODE Entry结构已经详细了解了,重点关注List Node for INODE Page List。
List Node for INODE Page List存储上一个INODE页和下一个INODE页的指针,用来构建SEG_INODES_FULL链表(该链表中的INODE类型的页中已经没有空闲空间来存储额外的INODE Entry结构)和SEG_INODES_FREE链表(该链表中的INODE类型的页中还有空闲空间来存储额外的INODE Entry结构了)。
《MySQL是怎么运行的》阅读分享_第32张图片
Segment Header 结构的运用

《MySQL是怎么运行的》阅读分享_第33张图片
其中的PAGE_BTR_SEG_LEAF和PAGE_BTR_SEG_TOP都占用10个字节,它们其实对应一个叫Segment Header的结构,该结构图示如下:

《MySQL是怎么运行的》阅读分享_第34张图片
《MySQL是怎么运行的》阅读分享_第35张图片
因为一个索引只对应两个段,所以只需要在索引的根页中记录这两个结构即可。

系统表空间额外存储的页
系统表空间和独立表空间的前三个页(页号分别为0、1、2,类型分别是FSP_HDR、IBUF_BITMAP、INODE)的类型是一致的,只是页号为3~7的页是系统表空间特有的

《MySQL是怎么运行的》阅读分享_第36张图片

《MySQL是怎么运行的》阅读分享_第37张图片
《MySQL是怎么运行的》阅读分享_第38张图片
除了这几个记录系统属性的页之外,系统表空间的extent 1和extent 2这两个区,也就是页号从64~191这128个页被称为Doublewrite buffer,也就是双写缓冲区。大部分知识都涉及到了事务和多版本控制的问题,这些问题我们会放在后边的章节集中介绍。

表的访问方法

单表访问方法

熟悉过记录结构、数据页结构以及索引的部分,在来看MySQL是怎么执行单表查询的。
准备数据:

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)概念

在表中查找目标数据的过程,即是访问方法。

查询的执行方式大致分为两种:

  • 使用全表扫描进行查询
    把表的每一行记录都扫一遍,过滤中目标数据
  • 使用索引进行查询

相对全表扫描,使用索引可以加快查询执行时间。利用索引查找的种类有:

  • 针对主键或唯一二级索引的等值查询
  • 针对普通二级索引的等值查询
  • 针对索引列的范围查询
  • 直接扫描整个索引

const
根据主键、普通唯一索引列等值匹配查询(is null除外),这种查询是很快的,查询速率认为是常数级别的,定义为const。

SELECT * FROM single_table WHERE id = 1438;

《MySQL是怎么运行的》阅读分享_第39张图片

SELECT * FROM single_table WHERE key2 = 3841;

《MySQL是怎么运行的》阅读分享_第40张图片

ref

根据普通的二级索引等值匹配,或is null。(前面说的普通唯一索引列查询时 is null也是这种场景)。这种方式需要先根据普通索引匹配到多个主键,然后根据主键进行回表。

SELECT * FROM single_table WHERE key1 = 'abc';![请添加图片描述](https://img-blog.csdnimg.cn/d46f492892a44cbca653768c24d8ca28.png)

《MySQL是怎么运行的》阅读分享_第41张图片

ref_or_null

根据普通的二级索引等值匹配并且条件里有or is null。

SELECT * FROM single_demo WHERE key1 = 'abc' OR key1 IS NULL;

《MySQL是怎么运行的》阅读分享_第42张图片

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';

all

全表扫描,直接扫描主键索引,这种访问方式称为all。

index merge

除此之外,还会有index merge(索引合并),针对一些and、or的操作,单纯的回表可能速度会慢一些,如果先将使用到的索引先进行求 交集、并集之后在进行回表,会更加高效。

SELECT * FROM single_table WHERE key1 = 'a' AND key3 = 'b';

两张表连接的原理

连接的本质

准备数据:

CREATE TABLE t1 (m1 int, n1 char(1));
CREATE TABLE t2 (m2 int, n2 char(1));

INSERT INTO t1 VALUES(1, 'a'), (2, 'b'), (3, 'c');
INSERT INTO t2 VALUES(2, 'b'), (3, 'c'), (4, 'd');

连接的本质就是把各个连接表中的记录都取出来依次匹配的组合加入结果集并返回给用户.
t1和t2两个表连接起来的过程如下图:
《MySQL是怎么运行的》阅读分享_第43张图片

连接过程

SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';

1.首先确定第一个需要查询的表,这个表称之为驱动表。再通过单表访问,查询目标结果集。
确定以t1表为驱动表,通过all方式访问该表。
《MySQL是怎么运行的》阅读分享_第44张图片
2.针对上一步骤中从驱动表产生的结果集中的每一条记录,分别需要到t2表中查找匹配的记录,所谓匹配的记录,指的是符合过滤条件的记录,这个过程还是单表访问。
《MySQL是怎么运行的》阅读分享_第45张图片

内连接和外连接

准备数据:

CREATE TABLE student (
    number INT NOT NULL AUTO_INCREMENT COMMENT '学号',
    name VARCHAR(5) COMMENT '姓名',
    major VARCHAR(30) COMMENT '专业',
    PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8 COMMENT '学生信息表';

CREATE TABLE score (
    number INT COMMENT '学号',
    subject VARCHAR(30) COMMENT '科目',
    score TINYINT COMMENT '成绩',
    PRIMARY KEY (number, score)
) Engine=InnoDB CHARSET=utf8 COMMENT '学生成绩表';

内连接
对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集,我们上面提到的连接都是所谓的内连接。

select student.*, score.*  from student join score where student.number ='123';

《MySQL是怎么运行的》阅读分享_第46张图片

外连接
对于外连接的两个表,驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集。

  • 左外连接(或左连接)
select * from student left join score on  Student .number = score .number 

《MySQL是怎么运行的》阅读分享_第47张图片- 右外连接(或右连接)

select * from student right join score on  Student .number = score .number 

《MySQL是怎么运行的》阅读分享_第48张图片

  • 完全外连接(或完全连接)(mysql中不支持,可以union,UNION 操作, 会合并掉重复的)
select * from student left join score on  Student .number = score .number 
union
select * from student right join score on  Student .number = score .number 

《MySQL是怎么运行的》阅读分享_第49张图片
WHERE和ON的区别
ON 是连接查询中的连接条件,就是驱动表中的数据去被驱动表中进行匹配的一种规则(匹配条件)。对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充。
WHERE 是对查询出来的结果集进行条件筛选,是先查询出所有的结果集然后再使用where来进行筛选的;不论是内连接还是外连接,凡是不符合WHERE子句中的过滤条件的记录都不会被加入最后的结果集。

在内连接中where和on效果是等价的,但是还是不建议写where;

连接的原理

嵌套循环连接(Nested-Loop Join)
对于两表连接来说,驱动表只会被访问一遍,但被驱动表却要被访问到好多遍,具体访问几遍取决于对驱动表执行单表查询后的结果集中的记录条数。这种连接执行方式称之为嵌套循环连接。

对于内连接,sql语句中不能决定驱动表,而是优化器根据执行计划选取的。
对于外连接,左外连接把sql中left join 前的表作为驱动,右外连接把sql中right join 前的表作为驱动表。
嵌套循环连接过程

  • 步骤1:选取驱动表,使用与驱动表相关的过滤条件,选取代价最低的单表访问方法来执行对驱动表的单表查询。
  • 步骤2:对上一步骤中查询驱动表得到的结果集中每一条记录,都分别到被驱动表中查找匹配的记录。

两个步骤都能通过索引进行优化查询,加快连接速度。
《MySQL是怎么运行的》阅读分享_第50张图片
基于块的嵌套循环连接
采用嵌套循环连接算法的两表连接过程中,被驱动表可是要被访问好多次的,如果这个被驱动表中的数据特别多而且不能使用索引进行访问,那就相当于要从磁盘上读好几次这个表,这个I/O代价就非常大了,所以我们得想办法:尽量减少访问被驱动表的次数。
MySQL中引入join buffer,它是执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集中的记录装在这个join buffer中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和join buffer中的多条驱动表记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的I/O代价
《MySQL是怎么运行的》阅读分享_第51张图片

InnoDB的BufferPool

对于使用InnoDB作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是InnoDB对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。
InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。

InnoDB为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存——Buffer Pool(缓存池)。

Buffer Pool内部组成

Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB。
 每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前面,缓存页被存放到 Buffer Pool 后边。
 控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息等等。
《MySQL是怎么运行的》阅读分享_第52张图片
free链表的管理

你可能感兴趣的:(笔记,mysql,数据库,java)