MySQL执行查询语句的方式称为访问方法或者访问类型。
1) const:通过主键或者唯一二级索引列来定位一条记录。
2) ref:搜索条件为二级索引列与常数进行等值比较,形成的扫描区间为单点扫描区间,采用二级索引来执行查询。
3) range:使用索引执行查询时,对应的扫描区间为若干个单点扫描区间或者范围扫描区间。
4) index:扫描全部二级索引记录,且使用覆盖索引;或者进行全表扫描时添加ORDER BY 主键。
5) all:全表扫描。
6) index_merge:索引合并。
1) Intersection索引合并。
对不同索引中扫描到的记录的id值取交集,只为这些id执行回表操作。
如果使用Intersection索引合并的方式执行查询,并且每个使用到的索引都是二级索引的话,则要求从每个索引中获取到的二级索引记录都是按照主键值排序的。
2) Union索引合并。
对从不同索引中扫描到的记录的id值取并集,再为去重后的id值执行回表操作。
如果使用Union索引合并的方式执行查询,并且每个使用到的索引都是二级索引的话,则要求从每个索引中获取到的二级索引记录都是按照主键值排序的。
3) Sort-Union索引合并。
先将从各个索引中扫描到的记录的主键值进行排序,再按照执行Union索引合并的方式执行查询。
如果连接查询的结果集中包含一个表中的每一条记录与另一个表中的每一条记录相互匹配的组合,那么这样的结果集就可以称为笛卡儿积。
SELECT * FROM t1, t2;
1. 首先确定第一个需要查询的表t1,这个表称为驱动表。
2. 每获得一条驱动表记录,就立即到被驱动表t2中寻找匹配的记录。
1) 内连接。
SELECT * FROM t1, t2;
或者
SELECT * FROM t1 [INNER] JOIN t2 [ON 连接条件] [WHERE 过滤条件];
对于内连接的两个表,若驱动表中的记录在被驱动表中找不到匹配的记录,则该记录不会加入到最后的结果集。
2) 外连接。
有时候,针对驱动表中的某条记录,即使在被驱动表中没有找到与之匹配的记录,也仍然需要把该驱动表记录加入到结果集。于是就有了外连接的概念。
外连接包括左连接和右连接(两者差不多,下文只介绍左连接)。
对于内连接来说,驱动表和被驱动表是可以互换的。但是对于外连接来说,由于驱动表中的记录即使在被驱动表中找不到符合ON子句连接条件的记录,也会被加入到结果集,此时驱动表和被驱动表的关系就很重要了。
SELECT * FROM t1 LEFT JOIN t2 ON 连接条件 [WHERE 连接条件];
左连接选取左侧的表t1为驱动表。
1) WHERE子句。
不论是内连接还是外连接,凡是不符合WHERE子句中过滤条件的记录都不会被加入到最后的结果集。
2) ON子句。
内连接中的ON子句和WHERE子句是等价的。
外连接必须使用ON子句来指出连接条件。
对于外连接的驱动表中的记录来说,如果无法在被驱动表中找到匹配ON子句中过滤条件的记录,那么该驱动表记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充。
1. 驱动表只访问一次,但被驱动表却可能访问多次,且访问次数取决于对驱动表执行单表查询后的结果集中有多少条记录。
2. 如果有3个表进行连接,那么前2个表的连接结果集作为新的驱动表,与第3个表进行连接。
这种连接执行方式称为嵌套循环连接。
由于被驱动表可能会访问多次,因此可以为被驱动表建立合适的索引以加快查询速度。
一条查询语句在MySQL中的执行成本是由两个方面组成的。
1) I/O成本。
当查询表中的记录时,需要先把数据或者索引加载到内存中,然后再进行操作。这个从磁盘到内存的加载过程损耗的时间称为I/O成本。
2) CPU成本。
读取记录以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称为CPU成本。
MySQL规定:读取一个页面花费的成本默认是1.0;读取以及检测一条记录是否符合搜索条件的成本默认是0.2。
1.0、0.2这些数字称为成本常数。
1. 根据搜索条件,找出所有可能使用的索引。
2. 计算全表扫描的代价。
3. 计算使用不同索引执行查询的代价。
4. 对比各种执行方案的代价,找出成本最低的那个方案。
I/O成本 = [聚簇索引占用的页面数] × 1.0 + [微调值1]
CPU成本 = [该表中的记录数] × 0.2 + [微调值2]
1) 聚簇索引占用的页面数。
SHOW TABLE STATUS LIKE [表名];
得到data_length,表示表占用的存储空间字节数,因为每个页面默认是16KB,所以
聚簇索引占用的页面数 = data_length ÷ 16 ÷ 1024
2) 该表中的记录数。
SHOW TABLE STATUS LIKE [表名];
得到rows,表示表中的记录条数。
3) 微调值。
本书的作者也不知道这玩意儿是干啥的,不用关心。
I/O成本 = [扫描区间的数量] + [预估的二级索引记录条数]
CPU成本 = [读取二级索引记录的成本] + [读取并检测回表操作后聚簇索引记录的成本]
1) 扫描区间的数量。
根据查询条件可以判断使用了多少个扫描区间。
MySQL简单粗暴地认为读取一个扫描区间的I/O成本与读取一个页面的I/O成本是相同的,所以这部分的I/O成本 = [扫描区间的数量] × 1.0。
2) 预估的二级索引记录条数。
先根据查询条件访问B+树索引,找到满足条件的第一条记录,称为区间最左记录。根据查询条件找到满足条件的最后一条记录,称为区间最右记录。如果区间最左记录和区间最右记录相隔不大于10个页面,可以精确地统计出两者之间二级索引记录的个数。否则,从区间最左记录向右读10个页面,计算每个页面平均包含多少记录,然后用平均值乘以两条记录之间的页面数量。
MySQL简单粗暴地认为每次回表操作都相当于访问一个页面,所以这部分的I/O成本 = [预估的二级索引记录条数] × 1.0。
3) 读取二级索引记录的成本。
等于[预估的二级索引记录条数] × 0.2 + [微调值]。
4) 读取并检测回表操作后聚簇索引记录的成本。
等于[预估的二级索引记录条数] × 0.2。
在MySQL中连接查询采用的是嵌套循环连接算法,驱动表会被访问一次,被驱动表会被访问多次。
对于两表连接查询来说,它的查询成本由两部分构成:
1) 单次查询驱动表的成本;
2) 多次查询被驱动表的成本。
我们把查询驱动表后得到的记录条数称为驱动表的扇出(fanout)。
复杂的查询条件下,计算扇出的算法比较复杂,本书没有过多介绍。
连接查询的成本计算公式是这样的:
连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出值 × 单次访问被驱动表的成本
在连接查询的成本中,占比 较大的部分是[驱动表扇出值 × 单次访问被驱动表的成本],所以我们的优化重点是:
1) 尽量减少驱动表的扇出;
2) 访问被驱动表的成本要尽量低。
SELECT * FROM mysql.server_cost;
SELECT * FROM mysql.engine_cost;
InnoDB提供了两种存储统计数据的方式,分别是永久性地存储统计数据和非永久性地存储统计数据。
1) 永久性地存储统计数据:统计数据存储在磁盘上,在服务器重启之后这些统计数据依然存在。
2) 非永久性地存储统计数据:统计数据存储在内存中,当服务器关闭时这些统计数据就被清除掉。等到服务器重启之后,在某些适当的场景下会重新收集这些统计数据。
我们可以在创建和修改表的时候,通过指定STATS_PERSISTENT属性来指明该表的统计数据的存储方式(1:永久存储 | 0:非永久存储)。
当我们选择把某个表以及该表索引的统计数据存放到磁盘上时,实际上是把这些统计数据存储到了两个表中:
innodb_index_stats:存储了关于表的统计数据。
innodb_table_stats:存储了关于索引的统计数据。
innodb_table_stats表的几个重要的统计项:
1) n_rows:表中记录的条数。
2) clustered_index_size:表的聚簇索引占用的页面数量。
3) sum_of_other_index_sizes:表的其他索引占用的页面数量。
innodb_index_stats中索引的统计项:
1) n_leaf_pages:索引的叶子节点实际占用的页面数量。
2) size:索引总共占用的页面数量。
3) n_diff_pfxNN:对应的索引列不重复的值有多少。
NN可以理解为联合索引的前N个列。
如果发生变动的记录数量超过了表大小的10%,并且自动重新计算统计数据的功能是打开的,那么服务器会重新计算一次统计数据。
已经过时了。
MySQL竭尽全力地把一些编写得很糟糕的语句转换成某种可以高效执行的形式,这个过程也可以称为查询重写。
虽然本章的标题和第十二章的标题“基于成本的优化”很相似,但是第十二章的内容是可以指导我们做查询优化的,而本章主要是对MySQL底层的一些优化方案做介绍,了解下就可以了。
1) 移除不必要的括号。
SELECT * FROM (t1, (t2, t3)) WHERE ...
=> SELECT * FROM t1, t2, t3 WHERE ...
2) 常量传递。
a = 5 AND b > a
=> a = 5 AND b > 5
3) 移除没用的条件。
(a < 1 AND b = b) OR (a = 6 OR 5 != 5)
=> a < 1 OR a = 6
4) 表达式计算。
a = 5 + 1
=> a = 6
5) HAVING子句和WHERE子句合并。
如果查询语句中没有出现诸如SUM、MAX这样的聚集函数以及GROUP BY子句,查询优化器就把HAVING子句和WHERE子句合并起来。
当我们在WHERE子句中指定“被驱动表的列不为NULL”时,外连接和内连接就没有区别了。
优化器就可以通过评估表的不同连接顺序的成本,选出成本最低的连接顺序来执行查询。
篇幅很长,不想看。
MySQL贴心地提供了EXPLAIN语句,可以让我们查看某个查询语句的具体执行计划,只需要在具体的查询语句前面加一个EXPLAIN就可以了。
EXPLAIN语句输出中的列包括:
1) id:在一个大的查询语句中,每个SELECT关键字对应一个唯一的id。
2) select_type:SELECT关键字对应的查询的类型。
3) table:表名。
4) partitions:匹配的分区信息。
5) type:针对单表的访问方法。
6) possible_keys:可能用到的索引。
7) key:实际使用的索引。
8) key_len:实际使用的索引长度。
9) ref:当使用索引列等值查询时,与索引列进行等值匹配的对象信息。
10) rows:预估的需要读取的记录条数。
11) filtered:针对预估的需要读取的记录,经过搜索条件过滤后剩余记录条数的百分比。
12) Extra:一些额外的信息。
无论查询语句有多复杂,里面包含了多少个表,到最后也是对每个表进行单表访问。
EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表该表的表名。
查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值。
对于连接查询来说,一个SELECT关键字后面的FROM子句中可以跟随多个表。在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id值都是相同的。
MySQL为每一个SELECT关键字代表的小查询都定义了一个名为select_type的属性,只要我们知道了某个小查询的select_type属性,也就知道了这个小查询在整个大查询中扮演一个什么角色。
1) SIMPLE。
查询语句中不包含UNION或者子查询的查询都算作SIMPLE类型。
2) PRIMARY和UNION。
对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的;
其中最左边的那个查询的select_type值就是PRIMARY;
除了最左边的小查询以外,其余小查询的select_type值就是UNION。
3) 其他。
本书到现在为止还没有讲过分区,跳过。
MySQL对某个表执行查询时的访问方法,在第十章曾经讲过一部分。
1) system。
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,那么对该表的访问方法就是system。
2) const。
当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const。
(只匹配到一条数据)
3) eq_ref。
执行连接查询时,如果被驱动表是通过主键或者不允许存储NULL值的唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是eq_ref。
(如果把驱动表的扇出看作匹配到的多条数据,就可以按照ref来理解)
4) ref。
当通过普通的二级索引列与常量进行等值匹配的方式来查询某个表时,对该表的访问方法就可能是ref。
(匹配到多条数据)
5) ref_or_null。
当对普通二级索引列进行等值匹配且该索引列的值可以为NULL值时,对该表的访问方法就可能是ref_or_null。
6) index_merge。
在某些场景下,使用Intersection、Union、Sort-Union这3种索引合并的方式来执行查询,对该表的访问方法就是index_merge。
7) range。
如果使用索引获取某些单点扫描区间的记录,那么就可能使用到range访问方法。
8) index。
当可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index。
9) ALL。
全表扫描。
10) 其他。
possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些;
key列表示实际用到的索引有哪些。
key_len列表示key占用的存储空间长度。
ref列展示的就是与索引列进行等值匹配的东西是啥,如const、NULL、具体的列、func。
rows列代表预计扫描的索引记录行数;
filtered列代表预计扫描的索引记录中满足其余搜索条件的记录的百分比。
种类很多,有几十种,如果有需要,可以翻书查一下。
在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON,可以得到一个JSON格式的执行计划。
JSON格式的执行计划里面包含了该计划花费的成本。
在MySQL 5.6 以及之后的版本中,MySQL提供了一个optimizer trace的功能,这个功能可以让用户方便地查看优化器生成执行计划的整个过程。
optimizer trace默认是关闭的,打开方式:
SET optimizer_trace="enabled=on";
每当查询语句执行完成后,就可以到information_schema数据库下的OPTIMIZER_TRACE表中查看完整的执行计划生成过程。
InnoDB存储引擎在处理客户端的请求时,如果需要访问某个页的数据,就会把完整的页中的数据全部加载到内存中。
为了缓存磁盘中的页,在MySQL服务器启动时会向操作系统申请一片连续的内存,称为Buffer Pool(缓冲池),默认大小为128MB。
Buffer Pool被划分成了若干个页面,页面大小与InnoDB表空间使用的页面大小一致,默认都是16KB,这些Buffer Pool中的页面称为缓冲页。
InnoDB为每一个缓冲页都创建了一些控制信息,我们把每个页对应的控制信息占用的一块内存称为一个控制块,控制块与缓冲页是一一对应的。
为了快速定位某个页是否被加载到Buffer Pool中,可使用 表空间号+页号 作为key,缓冲页控制块的地址作为value的形式来建立哈希表。