子查询结果集组成的表称之为
派生表
14.3.1.1
按返回的结果集区分子查询
标量子查询
那些只返回一个单一值的子查询称之为
标量子查询
,比如这样:
SELECT (SELECT m1 FROM t1 LIMIT 1);
或者这样:
SELECT * FROM t1 WHERE m1 = (SELECT MIN(m2) FROM t2);
这两个查询语句中的子查询都返回一个单一的值,也就是一个
标量
。这些标量子查询可以作为一个单一值或者表达式的一部分出现在查询语句的各个地方。
行子查询
顾名思义,就是返回一条记录的子查询,不过这条记录需要包含多个列(只包含一个列就成了标量子查询了)。比如这样:
SELECT * FROM t1 WHERE (m1, n1) = (SELECT m2, n2 FROM t2 LIMIT 1);
其中的
(SELECT m2, n2 FROM t2 LIMIT 1)
就是一个行子查询,整条语句的含义就是要从
t1
表中找一些记录,这些记录的 m1
和
n2
列分别等于子查询结果中的
m2
和
n2
列。
列子查询
列子查询自然就是查询出一个列的数据喽,不过这个列的数据需要包含多条记录(只包含一条记录就成了标量子查询了)。比如这样:
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2);
其中的
(SELECT m2 FROM t2)
就是一个列子查询,表明查询出
t2
表的
m2
列的值作为外层查询
IN
语句的参数。
表子查询
顾名思义,就是子查询的结果既包含很多条记录,又包含很多个列,比如这样:
SELECT * FROM t1 WHERE (m1, n1) IN (SELECT m2, n2 FROM t2);
14.3.1.2
按与外层查询关系来区分子查询
不相关子查询
如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询称之为
不相关子查询 。
相关子查询
如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为
相关子查询
。比如: SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2 WHERE n1 = n2);
14.3.1.3
子查询在布尔表达式中的使用
这里的子查询只能是标量子查询或者行子查询,也就是子查询的结果只能返回一个单一的值或者只能是一条记录
IN
或者
NOT IN
SELECT * FROM t1 WHERE (m1, n2) IN (SELECT m2, n2 FROM t2);
ANY/SOME
(
ANY
和
SOME
是同义词)
SELECT * FROM t1 WHERE m1 > ANY(SELECT m2 FROM t2);
ALL
SELECT * FROM t1 WHERE m1 > ALL(SELECT m2 FROM t2);
EXISTS
子查询
SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2);
14.3.1.4
子查询语法注意事项
子查询必须用小括号扩起来。
在 SELECT 子句中的子查询必须是标量子查询
mysql> SELECT (SELECT m1, n1 FROM t1);
ERROR 1241 (21000): Operand should contain 1 column(s)
在想要得到标量子查询或者行子查询,但又不能保证子查询的结果集只有一条记录时,应该使用
LIMIT 1
语句来限制记录数量。
对于
[NOT] IN/ANY/SOME/ALL
子查询来说,子查询中不允许有
LIMIT
语句
mysql> SELECT * FROM t1 WHERE m1 IN (SELECT * FROM t2 LIMIT 2);
ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SO
ME subquery'
不允许在一条语句中增删改某个表的记录时同时还对该表进行子查询
。 比方说这样:
mysql> DELETE FROM t1 WHERE m1 < (SELECT MAX(m1) FROM t1);
ERROR 1093 (HY000): You can't specify target table 't1' for update in FROM clause
14.3.2
子查询在
MySQL
中是怎么执行的
14.3.2.1
小白们眼中子查询的执行方式
14.3.2.2
标量子查询、行子查询的执行方式
SELECT * FROM s1 WHERE key1 = (SELECT common_field FROM s2 WHERE key3 = 'a' LIMIT 1);
对于包含不相关的标量子查询或者行子查询的查询语句来说,
MySQL
会分别独立的执行外层查询和子查询,就当作两个单表查询就好了
对于
相关
的标量子查询或者行子查询来说
14.3.2.3 IN
子查询优化
物化表的提出
SELECT * FROM s1
WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');
不直接将不相关子查询的结果集当作外层查询的参数,而是将该结果集写入一个临时表里
该临时表的列就是子查询结果集中的列。
写入临时表的记录会被去重。
建立基于内存的使用
Memory
存储引擎的临时表,而且会为该表建立
哈希索引
如果子查询的结果集非常大,超过了系统变量
tmp_table_size
或者
max_heap_table_size
,临时表会转而使用基于磁盘的存储引擎来保存结果集中的记录,
索引类型也对应转变为 B+ 树索引
将子查询结果集中的记录保存到临时表的过程称之为
物化
物化表转连接
将子查询转换为semi-join
半连接的意思就是:
对于
s1
表的某条记录来说,我们只关心在
s2
表中是否存在与之匹配的记录是否存在,而不关心具体有多少条记录与之匹配,最终的结果集中只保留
s1
表的记录
由于相关子查询并不是一个独立的查询,所以不能转换为物化表来执行查询
不适用于semi-join的情况
如果
IN
子查询不满足转换为
semi-join
的条件,又不能转换为物化表或者转换为物化表的
成本太大,那么它就会被转换为
EXISTS
查询
14.3.2.4 ANY/ALL
子查询优化
14.3.2.5 [NOT] EXISTS
子查询的执行
14.3.2.6
对于派生表的优化
15
第
15
章 查询优化的百科全书
-Explain
详解(上)
列名
描述
id 在一个大的查询语句中每个 SELECT
关键字都对应一个唯一的
id
select_type SELECT 关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys
可能用到的索引
key 实际上使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息
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;
15.1
执行计划输出中各列详解
15.1.1 table
不论我们的查询语句有多复杂,里边儿包含了多少个表,到最后也是需要对每个表进行单表访问的,所以设计MySQL 的大叔规定
EXPLAIN
语句输出的每条记录都对应着某个单表的访问方法,该条记录的
table
列代表着该表的表名
15.1.2 id
查询语句中每出现一个
SELECT
关键字,设计
MySQL
的大叔就会为它分配一个唯一的
id
值
对于连接查询来说,一个
SELECT
关键字后边的
FROM
子句中可以跟随多个表,所以在连接查询的执行计划中,
每个表都会对应一条记录,但是这些记录的id
值都是相同的
在连接查询的执行计划中,每个表都会对应一条记录,这些记录的
id
列的值是相同的,出现在前边的表表示驱动表,出现在后边的表表示被驱动表
查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询
如果查询语句是一个子查询,但是执行计划中
s1
和
s2
表对应的记录的
id
值全部是
1
,这就
表明了
查询优化器将子查询转换为了连接查询
UNION 子句是为了把
id
为
1
的查询和
id
为
2
的查询的结果集合并起来并去重,所以在内部创建了一个名为 <union1, 2>
的临时表
UNION ALL 就不需要为最终的结果集进行去重,没有那个 id 为 NULL
的记录
15.1.3 select_type
SIMPLE
查询语句中不包含 UNION
或者子查询的查询都算作是
SIMPLE 类型,连接查询也算是
SIMPLE
类型
PRIMARY
对于包含
UNION
、
UNION ALL
或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的 select_type
值就是
PRIMARY
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
UNION
对于包含
UNION
或者
UNION ALL
的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的小查询的 select_type
值就是
UNION
UNION RESULT
MySQL
选择使用临时表来完成
UNION
查询的去重工作,针对该临时表的查询的
select_type
就是
UNIONRESULT
SUBQUERY
如果包含子查询的查询语句不能够转为对应的
semi-join
的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个 SELECT
关键字代表的那个查询的 select_type
就是
SUBQUERY
由于
select_type
为
SUBQUERY
的子查询由于会被物化,所以只需要执行一遍
DEPENDENT SUBQUERY
如果包含子查询的查询语句不能够转为对应的
semi-join
的形式,并且该子查询是相关子查询,则该子查询的第一个 SELECT
关键字代表的那个查询的
select_type
就是
DEPENDENT SUBQUERY
select_type
为
DEPENDENT SUBQUERY
的查询可能会被执行多次
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2) OR key3 = 'a';
DEPENDENT UNION
在包含
UNION
或者
UNION ALL
的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的 select_type
的值就是
DEPENDENT UNION
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b');
DERIVED
对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的
select_type
就是
DERIVED。
物化之后再连接
mysql> EXPLAIN SELECT * FROM (SELECT key1, count(*) as c FROM s1 GROUP BY key1) AS derived_s1 where c > 1;
UNCACHEABLE SUBQUERY
不常用,就不多唠叨了。
UNCACHEABLE UNION
不常用,就不多唠叨了
15.1.4 partitions
由于我们压根儿就没唠叨过分区是个啥,所以这个输出列我们也就不说了哈,一般情况下我们的查询语句的执行计划的 partitions
列的值都是
NULL
。
15.1.5 type
代表着
MySQL
对某个表的执行查询时的访问方法,所有存储引擎访问方法如下:
system
,
const
, eq_ref ,
ref
,
fulltext
,
ref_or_null
,
index_merge
,
unique_subquery
,
index_subquery
, range ,
index
,
ALL
system
当表中只有一条记录并且
该表使用的存储引擎的统计数据是精确的,比如
MyISAM
、
Memory
const
当我们根据主键或者唯一二级索引列与常数进行等值匹配
eq_ref
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该
被驱动表
的访问方法就是eq_ref
ref
当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么该表的访问方法就
可能
是
ref
fulltext
全文索引
ref_or_null
当对普通二级索引进行等值匹配查询,该索引列的值也可以是
NULL
值时,那么对该表的访问方法就
可能
是ref_or_null
index_merge
一般情况下对于某个表的查询只能使用到一个索引,但我们唠叨单表访问方法时特意强调了在某些场景下可以使用 Intersection
、
Union
、
Sort-Union
这三种索引合并的方式来执行查询
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
unique_subquery
类似于两表连接中被驱动表的
eq_ref
访问方法,
unique_subquery
是针对在一些包含
IN
子查询的查询语句中,如果查询优化器决定将 IN
子查询转换为
EXISTS
子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的 type
列的值就是
unique_subquery
index_subquery
index_subquery
与
unique_subquery
类似,只不过访问子查询中的表时使用的是普通的索引
range
如果使用索引获取某些
范围区间
的记录,那么就
可能
使用到
range
访问方法
index
当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是
index
ALL
最熟悉的全表扫描
15.1.6 possible_keys
和
key
possible_keys
列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些, key
列表示实际用到的索引有哪些
possible_keys
列中的值并不是越多越好,可能使用的索引越多,查询优化器计算查询成本时就得花费更长时间,所以如果可以的话,尽量删除那些用不到的索引
15.1.7 key_len
key_len
列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,它是由这三个部分构成的:
对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是 VARCHAR(100)
,使用的字符集是
utf8
,那么该列实际占用的最大存储空间就是 100 × 3 = 300
个字节。
如果该索引列可以存储
NULL
值,则
key_len
比不可以存储
NULL
值时多
1
个字节。
对于变长字段来说,都会有
2
个字节的空间来存储该变长列的实际长度
执行计划的生成是在MySQL server 层中的功能,并不是针对具体某个存储引擎的功能,设计
MySQL
的大叔在执行计划中输出key_len 列主要是为了让我们区分某个使用联合索引的查询具体用了几个索引列
15.1.8 ref
当使用索引列等值匹配的条件去执行查询时,也就是在访问方法是
const
、
eq_ref
、
ref
、
ref_or_null
、unique_subquery 、
index_subquery
其中之一时,
ref
列展示的就是与索引列作等值匹配的东东是个啥
15.1.9 rows
如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的
rows
列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的 rows
列就代表预计扫描的索引记录行数
15.1.10 filtered
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND common_field = 'a';
执行计划的
filtered
列就代表查询优化器预测在这
266
条记录中,有多少条记录满足其余的搜索条件,也就是 common_field = 'a'
这个条件的百分比
对于单表查询来说,这个
filtered
列的值没什么意义
16
第
16
章 查询优化的百科全书
-Explain
详解(下)
16.1.1 Extra
No tables used
当查询语句的没有
FROM
子句时将会提示该额外信息
Impossible WHERE
查询语句的
WHERE
子句永远为
FALSE
时将会提示该额外信息
No matching min/max row
当查询列表处有
MIN
或者
MAX
聚集函数,但是并没有符合
WHERE
子句中的搜索条件的记录时,将会提示该额外信息
Using index
当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用索引覆盖的情况下,在Extra 列将会提示该额外信息
Using index condition
对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足
key1 LIKE '%a' 这个条件,之后再回表。这个改进称之为
索引条件下推
在查询语句的执行过程中将要使用 索引条件下推 这个特性,在 Extra 列中将会显示 Using index condition
Using where
当我们使用全表扫描来执行对某个表的查询,并且该语句的
WHERE
子句中有针对该表的搜索条件时,在Extra 列中会提示上述额外信息
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;
Using join buffer (Block Nested Loop)
在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,
MySQL
一般会为其分配一块名叫join buffer 的内存块来加快查询速度
Not exists
当我们使用左(外)连接时,如果
WHERE
子句中包含要求被驱动表的某个列等于
NULL
值的搜索条件,而且那个列又是不允许存储 NULL
值的,那么在该表的执行计划的
Extra
列就会提示
Not exists
额外信息
mysql> EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
Using intersect(...)
、
Using union(...)
和
Using sort_union(...)
如果执行计划的
Extra
列出现了
Using intersect(...)
提示,说明准备使用
Intersect
索引合并的方式执行查询,括号中的 ...
表示需要进行索引合并的索引名称;如果出现了
Using union(...)
提示,说明准备使用 Union
索引合并的方式执行查询;出现了
Using sort_union(...)
提示,说明准备使用
Sort-Union
索引合并的方式执行查询。
Zero limit
当我们的
LIMIT
子句的参数为
0
时
Using filesort
如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的
Extra
列中显示
Using filesort
Using temporary
在许多查询的执行过程中,
MySQL
可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含 DISTINCT
、
GROUP BY
、
UNION
等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL 很有可能寻求通过建立内部的临时表来执行查询
MySQL 会在包含 GROUP BY子句的查询中默认添加上 ORDER BY 子句
如果我们并不想为包含
GROUP BY
子句的查询进行排序,需要我们显式的写上
ORDER BY NULL
执行计划中出现 Using temporary 并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以我们最好能使用索引来替代掉使用临时表
Start temporary, End temporary
我们前边唠叨子查询的时候说过,查询优化器会优先尝试将
IN
子查询转换成
semi-join
,而
semi-join
又有好多种执行策略,当执行策略为 DuplicateWeedout
时,也就是通过建立临时表来实现为外层查询中的记录进行去重操作时,驱动表查询执行计划的 Extra
列将显示
Start temporary
提示,被驱动表查询执行计划的 Extra
列将显示
End temporary
LooseScan
在将
In
子查询转为
semi-join
时,如果采用的是
LooseScan
执行策略
FirstMatch(tbl_name)
在将
In
子查询转为
semi-join
时,如果采用的是
FirstMatch
执行策略
16.2 Json
格式的执行计划
在
EXPLAIN
单词和真正的查询语句中间加上
FORMAT=JSON
。这样我们就可以得到一个 json
格式的执行计划,里边儿包含该计划花费的成本
mysql> EXPLAIN FORMAT=JSON SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field = 'a'
16.3 Extented EXPLAIN
在我们使用
EXPLAIN
语句查看了某个查询的执行计划后,紧接着还可以使用 SHOW WARNINGS
语句查看与这个查询的执行计划有关的一些扩展信息
17
第
17
章 神兵利器
-optimizer trace
表的神器功效
optimizer trace
的功能可以让我们方便的查看优化器生成执行计划的整个过程,这个功能的开启与关闭由系统变量optimizer_trace 决定
完整的使用
optimizer trace
功能的步骤总结如下:
# 1. 打开optimizer trace功能 (默认情况下它是关闭的):
SET optimizer_trace="enabled=on";
# 2. 这里输入你自己的查询语句
SELECT ...;
# 3. 从OPTIMIZER_TRACE表中查看上一个查询的优化过程
SELECT * FROM information_schema.OPTIMIZER_TRACE;
# 4. 可能你还要观察其他语句执行的优化过程,重复上边的第2、3步
...
# 5. 当你停止查看语句的优化过程时,把optimizer trace功能关闭
SET optimizer_trace="enabled=off";
18
第
18
章 调节磁盘和
CPU
的矛盾
-InnoDB
的
Buffer
18.1
缓存的重要性
即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中
。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其 缓存
起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO
的开销
18.2 InnoDB
的
Buffer Pool
18.2.1
啥是个
Buffer Pool
设计
InnoDB
的大叔为了缓存磁盘中的页,在
MySQL
服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做 Buffer Pool
(中文名是
缓冲池 ),默认情况下
Buffer Pool
只有
128M
大小
每个页对应的控制信息占用的一块内存称为一个控制块 吧,
控制块和缓存页是一一对应的,它们都被存放到
Buffer Pool
中,其中控制块被存放到
Buffer Pool的前边,缓存页被存放到 Buffer Pool
后边
18.2.3 free
链表的管理
我们最好在某个地方记录一下
Buffer Pool
中哪些缓存页是可用的
,这个时候缓存页对应的
控制块
就派上大用场了,我们可以
把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中
,这个链表也可以被称作
free链表
(或者说空闲链表)
18.2.4
缓存页的哈希处理
18.2.5 flush
链表的管理
如果我们修改了
Buffer Pool
中某个缓存页的数据,那它就和磁盘上的页
不一致
了,这样的缓存页也被称为
脏页 (英文名:
dirty page
)
凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush链表
18.2.6 LRU
链表的管理
18.2.6.1
缓存不够的窘境
free链表
中已经没有多余的空闲缓存页的时候
只要我们使用到某个缓存页,就把该缓存页调整到
LRU链表
的头部,这样
LRU链表
尾部就是最近最少使用的缓存页喽,
当
Buffer Pool
中的空闲缓存页使用完时,到
LRU链表
的尾部找些缓存页淘汰就
OK
啦
18.2.6.3
划分区域的
LRU
链表
将
LRU
链表划分为
young
和
old
区域这两个部分,又添加了
innodb_old_blocks_time
这个系统变量,使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,因为用不到的预读页面以及全表扫描的页面都只会被放到 old 区域,而不影响
young
区域中的缓存页
18.2.6.4
更进一步优化
LRU
链表
如只有被访问的缓存页位于
young
区域的
1/4
的后边,才会被移动到
LRU链表
头部,这样就可以降低调整 LRU链表
的频率,从而提升性能
尽量高效的提高