实战篇:MySQL优化系列--SQL优化实战

Posted 进击的CJR

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战篇:MySQL优化系列--SQL优化实战相关的知识,希望对你有一定的参考价值。

前言

mysql的SQL优化利器--执行计划对于SQL优化有很大帮助,MySQL由于体系结构的原因,优化最常见的比如加索引,可能就能完成一次质的优化。那么如何看执行计划,如何通过执行计划进行SQL优化呢,本文带了实战详解。

本文理论方面参考

《MySQL实战45讲》--丁奇

《MySQL是怎样运行的--从根儿上理解MySQL》 --小孩子4919

本文通过解读执行计划,和实验演示的方式,将说明如下内容:

  • 执行计划怎么看?需要重点关注那些列?
  • mysql连接查询的原理是什么样的?
  • mysql对子查询有什么优化?
  • 联合索引如何优化?
  • 子查询如何优化?
  • group by如何优化?
  • limit如何优化?
  • in和exists如何选?

执行计划详解

执行计划包含如下列

|id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra|

其中最重要的是查询表的扫描方式

type列

const
通过主键或者唯一二级索引列与常数的等值比较来定位一条记录的访问方法定义为: const ,意思是常数级别的,代价是可以忽略不计的。

system
表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计。

ref
搜索条件为二级索引列与常数进行等值查询比较,形成的扫描区间为单点扫描区间,采用二级索引来执行查询的访问方法称为ref。

ref_or_null
在ref的基础上,同时查找该列中值为null的记录,对应的扫描区间是[NULL,NULL]以及等值的单点扫描区间。
注:在二级索引树的结构上,值为NULL的记录会被放在索引的最左边。

eq_ref
多表连接,被驱动表连接条件是主键或不允许为NULL唯一键的等值匹配进行访问。
唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描(不含NULL的唯一二级索引)

index_merge
mysql在一般情况下,只会为当个索引生成扫描区间,也可能为多个索引生成扫描区间,使用多个索引来完成一次查询的执行方法叫index merge 索引合并。

range
使用索引执行查询时,对应的扫描区间为若干个单点扫描区间或者范围扫描区间。(不包含一个单点区间,或者全表范围查询)

index
二级索引记录比聚簇索引记录小得多,聚簇索引要存储用户定义的所有列,二级索引只需要存放索引列和主键,而且这个过程不用执行回表操作。所以直接扫描全部的二级索引记录比直接扫描全部的聚簇索引成本要小很多。
这种扫描全部二级索引的访问称为index访问方法。
比如:联合索引(a,b,c) select a,b,c from t where b=;这种破坏了联合索引最左前缀法则,所以优化器会选择index查询方法。
order by 主键是典型的index访问方法。

all
全表扫描

在性能方面:
system>const>eq_ref>ref>range>index>ALL


索引合并index merge

mysql在一般情况下,只会为当个索引生成扫描区间,也可能为多个索引生成扫描区间,使用多个索引来完成一次查询的执行方法叫index merge 索引合并。

Intersection索引合并
Intersection对不同索引中扫描到的记录的id值取交集,只为这些id值执行回表操作。使用Intersection执行查询,并且每个使用到的索引都是二级索引,要求从每个索引中获取到的二级索引记录都是按照主键值进行排序的。
单列索引 a,b,主键id
select * from t where a= and b=;

Union索引合并
select * from t where a= or b=;
分别扫描二级索引,根据二级索引id值在两者的结果中进行去重,再根据去重后的id值执行回表操作。同样需要每个使用到的索引都是二级索引,并且要求从每个索引中获取到的二级索引记录都是按照主键值进行排序的。

Sort-Union索引合并
select * from t where a < or b b>;
先将从各个索引中扫描到的记录的主键值进行排序,再按照执行union索引合并的方式执行查询。比union索引合并多了一步对二级索引记录的主键值进行排序的过程。


id列

每个select关键字对应一个id值
出现在前面的表表示是驱动表,出现在后面的表表示被驱动表。
查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。如果在一个执行计划中,id都显示1,表明查询优化器将子查询转换为了连接查询。
id为NULL表明使用了临时表,比如将union将两个查询的结果集合并并去重。而UNION ALL不需要去重,所以不需要使用临时表,因此没有id 为NULL的这一行。


select_type列

每个id对应的selec的查询类型

simple            简单查询
primary          包含union union all或者子查询的大查询来说,包含很多小查询,小查询最左边的查询为primary
union          包含union union all或者子查询的大查询来说,其他小查询最左边的查询
union result union   去重工作
subquery 如果包含子查询的查询不能转换为半连接,并且该子查询是不相关子查询。优化器选择将子查询物化,则该子查询的第一个select为subquery
dependent subquery    如果包含子查询的查询不能转换为半连接,并且该子查询是相关子查询。则该子查询的第一个select为dependent subquery,该类型可能会被执行多次。
dependent union 包含union union all的大查询中,如果各个小查询都依赖于外层查询,则除了最左边的小查询外,其他小查询的select_type是dependent union
derived 在包含派生表的查询中,如果是以物化派生表的方式进行查询,则派生表对应的子查询是derived
materialized 当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询,则该子查询对应materialized


key_len列

实际使用的索引长度计算方式

char和varchar类型key_len计算公式:
varchr(N)变长字段且允许NULL = N *( character set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)
varchr(N)变长字段且不允许NULL = N * ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)

char(N)固定字段且允许NULL = N* ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)
char(N)固定字段且不允许NULL = N* ( character set:utf8=3,gbk=2,latin1=1)

值数据的key_len计算公式:
TINYINT允许NULL = 1 +1(NULL)
TINYINT不允许NULL = 1

SMALLINT允许为NULL =2+1(NULL)
SMALLINT不允许为NULL = 2

INT允许为NULL =4+1(NULL)
INT不允许为NULL = 4

BIGINT允许为NULL = 8+1(NULL)
BIGINT不允许为NULL = 8

日期时间型的key_len计算:(针对mysql5.6.4及之后的版本)
DATETIME允许为NULL= 5 + 1(NULL)
DATETIME不允许为NULL = 5
TIMESTAMP允许为NULL = 4+ 1(NULL)
TIMESTAMP不允许为NULL = 4

decimal
根据官方文档可以知道,decimal定义为decimal(M,D),其中,M是总的位数,D是小数点后保留的位数。
小数点前与小数点后的数字分开存储,且以9位数为1组,用4个字节保存,如果低于9位数,需要的字节数如下:
Leftover Digits Number of Bytes
-----------------------------
|0 |0 |
|1-2 |1 |
|3-4 |2 |
|5-6 |3 |
|7-9 |4 |
-----------------------------
例如:
•decimal(20,6)=> 小数点左边14位,小数点右边6位 => 小数点左边分组为5 + 9,需要3个字节+4个字节存储,小数点右边一个分组,需要3个字节存储 => 总共需要10个字节
•decimal(18,9)=> 小数点左边9位数,小数点右边9位数 => 分别使用4个字节存储 => 共需要 8个字节
•decimal(18,2)=> 小数点左边16位数,小数点右边2位数 => 分组为7 + 9,需要8个字节存储,小数点右边1个字节存储 => 共需要9个字节


实验前准备数据

create table test(
id int(11) not null auto_increment,
c1 varchar(10) default null,
c2 varchar(10) default null,
c3 varchar(10) default null,
c4 varchar(10) default null,
c5 varchar(10) default null,
primary key(id)
)engine=innodb default charset=utf8;

insert into test(c1,c2,c3,c4,c5) values (a1,a2,a3,a4,a5);
insert into test(c1,c2,c3,c4,c5) values (b1,b2,b3,b4,b5);
insert into test(c1,c2,c3,c4,c5) values (c1,c2,c3,c4,c5);
insert into test(c1,c2,c3,c4,c5) values (d1,d2,d3,d4,d5);
insert into test(c1,c2,c3,c4,c5) values (e1,e2,e3,e4,e5);
insert into test(c1,c2,c3,c4,c5) values (f1,f2,f3,f4,f5);

varchr(N)变长字段且允许NULL = N *( character set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)
因此算出一个c列的key_len=10*(3)+1+2=33
主键的key_len=4


联合索引优化

(1)联合索引等值查询

创建一个联合索引

mysql> create index idx_test_c1234 on test(c1,c2,c3,c4);

实战篇:MySQL优化系列--SQL优化实战_SQL优化

key_len=132(33*4)   c1,c2,c3,c4的key_len加起来刚好132符合预期

mysql> explain select * from test where c1=a1 and c4=a4 and c2=a2 and c3=a3;

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_02

同样,key_len=132   c1,c2,c3,c4的key_len加起来刚好132符合预期,说明条件书写顺序并不影响对联合索引的使用

结论:

创建联合索引的顺序为c1,c2,c3,c4。

执行结果type=ref,key_len=132。在执行等值查询的时候,查询条件书写顺序不影响对联合索引的使用,因为执行 sql 的时候,MySQL优化器会帮我们调整 where 后条件的顺序,让我们用上索引。


(2)联合索引范围查询

​mysql> explain select * from test where c1=a1 and c2=a2 and c3>a3 and c4=a4;​

实战篇:MySQL优化系列--SQL优化实战_SQL优化_03

mysql> explain select * from test where c1=a1 and c2=a2;

实战篇:MySQL优化系列--SQL优化实战_优化案例_04

结论:

当出现范围的时候,type=range ,key_len=99,比不用范围key_len=66增加了,说明使用上了索引,c4的索引失效。

范围右边索引列失效,但是范围当前位置的索引是有效的。


explain select * from test where c1=a1 and c2=a2 and c4>a4 and c3=a3;

实战篇:MySQL优化系列--SQL优化实战_执行计划_05

与上面的执行结果相比,key_len=132说明索引用到了4个,因此对此sql语句mysql底层优化器会进行优化:

范围右边索引列失效(c4右边已经没有索引列了),注意索引顺序(c1,c2,c3,c4),所以c4右边不会出现失效的索引列,因此4个索引全部用上。


结论:

范围右边索引列失效,是有顺序的:c1,c2,c3,c4,如果c3有范围,则c4失效,如果c4有范围,则没有失效的索引列,从而会使用全部索引


(3)联合索引的最左匹配原则

mysql>  explain select * from test where c1>a1 and c2=a2 and c3=a3 and c4=a4;

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_06

如果在c1处使用范围,则type=all,key=null,索引失效,全表扫描,这里违背了最左前缀法则,带头大哥已死,因为c1主要用于范围,而不是查询,解决方式使用覆盖索引。

结论:联合索引遵循最左前缀法则,如果最左前列的索引失效,则后面的索引都失效。


联合索引的最左匹配原则

​索引的目的其实就是为了提高数据查询的效率,联合索引也一样,使用联合索引时,一定要注意符合最左匹配原则:​

​在通过联合索引检索数据时,从索引中最左边的列开始,一直向右匹配,如果遇到范围查询(>、<、between、like等),就停止后边的匹配。​


前面我们实验范围当前位置是索引有效的,为什么这里无效呢?

这是因为mysql在查询结果集在达到全表数据>15-30%,优化器有可能会选择全表,如下,改成小于,就使用上了c1索引

mysql> explain select * from test where c1<a1 and c2=a2 and c3=a3 and c4=a4;

实战篇:MySQL优化系列--SQL优化实战_SQL优化_07

mysql> explain select * from test where  c1=a1 and c2=a2  and c4=a4 order by c3;

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_08

去掉c4条件进行验证

mysql> explain select * from test where  c1=a1 and c2=a2 order by c3;

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_09

​mysql> explain select * from test where c1=a1 and c2=a2 order by c4;​

用上了排序Using filesort,因为跳过了c3,联合索引是按c1,c2,c3,c4有序组成的,依次有序,而跳过c3,则c4无序了。

实战篇:MySQL优化系列--SQL优化实战_SQL优化_10

同样c3,c2位置颠倒

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_11

mysql> explain select * from test where  c1=a1 and c5=a5 order by c2,c3;
mysql> explain select * from test where c1=a1 and c5=a5 order by c3,c2;

再次验证,索引的顺序是有序的,所以使用c3,c2排序,导致排序的时候位置颠倒,需要额外的排序。

实战篇:MySQL优化系列--SQL优化实战_优化案例_12

mysql> explain select * from test where  c1=a1 and c2=a2 order by c2,c3;
同理,c2,c3按照联合索引顺序排序,不需要额外排序

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_13

mysql>  explain select * from test where  c1=a1 and c2=a2 and c5=a5 order by c3,c2;

因为c2是常量,在排序中被优化,索引位置并未颠倒,所以并没有使用到额外排序

实战篇:MySQL优化系列--SQL优化实战_执行计划_14

结论:

利用最左前缀法则:中间兄弟不能断,因此用到了c1和c2的索引,从key_len=66 ,c4并没有用上,但是extra列并没有显示使用排序,c3索引列用在排序过程中,因为联合索引是按c1,c2,c3,c4有序组成的

另外可以看到Using index condition 表示的是使用到了索引下推。


(4)使用覆盖索引优化

mysql> explain select * from test where  c1>a1 order by c1;

实战篇:MySQL优化系列--SQL优化实战_执行计划_15

使用覆盖索引

实战篇:MySQL优化系列--SQL优化实战_优化案例_16

mysql> explain select c1 from test order by c1 asc,c2 desc;

实战篇:MySQL优化系列--SQL优化实战_优化案例_17

这里order by 默认是升序,但是desc了c2,导致与索引的排序方式颠倒,因此使用上排序。

mysql> explain select c1 from test where c1 in (a1,b1) order by c2,c3;

实战篇:MySQL优化系列--SQL优化实战_执行计划_18

使用了 filesort 可以看出mysql对于多个值的等值查询也等价于范围查询。



连接查询

(1)笛卡尔积

如果连接查询的结果集中包含一个表中的每一条记录与另一个表中的每一条记录相互匹配的组合,那么这样的结果集就可以称为笛卡尔积。

连接任意数量的表,如果不附加任何限制条件,这些表连接起来产生的笛卡尔积可能是非常巨大的,3个100行表产生100*100*100=1000000 百万级的行记录。所以在连接时过滤掉特定的记录组合。


(2)什么是驱动表,被驱动表

连接查询中,首先确定第一个需要查询的表,称为驱动表。

从驱动表中每获取到一条记录,都需要到被驱动表中匹配记录。因此,在两表的连接查询中,驱动表只需要访问一次,被驱动表可能需要访问多次。

因为mysql机制是,每获得一条驱动表记录,就立刻到被驱动表中寻找匹配的记录。


(3)什么是内连接和外连接

内连接:若驱动表中的记录在被驱动表中找不到匹配的记录,则该记录不会加入到最后的结果集。

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

外连接分为两种,左外连接(左侧表为驱动表),右外连接(右侧表为驱动表)。


(4)连接查询的原理

NLJ

嵌套循环连接(NLJ)全称Index Nested-Loop Join

两个表连接查询,驱动表全表扫描,被驱动表的条件过滤列有索引,

这个过程是先遍历驱动表,表然后根据从驱动表 中取出的每行数据中的值,去被驱动表 中查找满足条件的记录。在形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引。

注:驱动表只访问一次,但是被驱动表却可能访问多次,且访问次数取决于对驱动表执行单表查询后的结果集中有多少条记录。

被驱动表上的索引可以减少扫描范围,但是可能会出现部分列不包含在索引导致全表扫描的情况,因此尽量避免使用select * from,而是只把真正用到列作为查询列表。


SNL

全称Simple Nested-Loop Join

和NLJ类似,不同的是被驱动表连接条件没有索引,导致被驱动表也全表扫描。这种扫描方式导致的扫描次数是 N*M次,而NLJ扫描次数是N+N(假设驱动表N行,被驱动表M行)

伪代码如下

A simple nested-loop join (NLJ) 
使用MySQL文档中伪代码的描述:
for each row in t1 matching range
for each row in t2 matching reference key
for each row in t3
if row satisfies join conditions,
send to client



这种方式采用逐层调用循环的方式,这种方式适用于任何场景,
不过在被连接表没有索引的情况下,那么效率极低。


BNL

基于块的嵌套循环连接(BNLJ) 全称Block Nested-Loop Join

被驱动表无法走索引,查询范围大,内存不够,且被驱动表被访问多次,会导致从磁盘读数据很多次,IO代价高。

将每一条驱动表的结果集数据放入内存(Join Buffer)中一次性与被驱动表进行匹配,显著减少IO代价。

join buffer 只存放查询列表的列和与被驱动表过滤条件的列,因此,不要使用select * 。

两个表都做一次全表扫描,所以总的扫描行数是 M+N;内存中的判断次数是 M*N。

实战篇:MySQL优化系列--SQL优化实战_执行计划_19

BNL 算法:

将外层循环的行/结果集存入join buffer,尽量减少访问被驱动表的次数。

内层循环的每一行与整个buffer中的记录做比较,从而减少内层循环的次数。


扫描一个表的过程其实是先把这个表从磁盘上加载到内存中,然后从内存中比较匹配条件是否满足。但内存里可能并不能完全存放的下表中所有的记录,所以在扫描表前边记录的时候后边的记录可能还在磁盘上,等扫描到后边记录的时候可能内存不足,所以需要把前边的记录从内存中释放掉。我们前边又说过,采用Simple Nested-Loop Join算法的两表联接过程中,被驱动表可是要被访问好多次的,如果这个被驱动表中的数据特别多而且不能使用索引进行访问,那就相当于要从磁盘上读好几次这个表,这个I/O代价就非常大了,

所以我们得想办法:尽量减少访问被驱动表的次数。

当被驱动表中的数据非常多时,每次访问被驱动表,被驱动表的记录会被加载到内存中,在内存中的每一条记录只会和驱动表结果集的一条记录做匹配,之后就会被从内存中清除掉。然后再从驱动表结果集中拿出另一条记录,再一次把被驱动表的记录加载到内存中一遍,周而复始,驱动表结果集中有多少条记录,就得把被驱动表从磁盘上加载到内存中多少次。所以我们可不可以在把被驱动表的记录加载到内存的时候,一次性和多条驱动表中的记录做匹配,这样就可以大大减少重复从磁盘上加载被驱动表的代价了。这也就是Block Nested-Loop Join算法的思想。

BNL 算法对系统的影响主要包括三个方面:

1、可能会多次扫描被驱动表,占用磁盘 IO 资源;

2、判断 join 条件需要执行 M*N 次对比(M、N 分别是两张表的行数),如果是大表就会占用

非常多的 CPU 资源;

3、可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率。



BKA

全称Batched Key Access

前面说了,不管NLJ还是BNL都可能导致被驱动表却可能访问多次(BNL中如果join buffer不够大也会导致,但是可以考虑增大join_buffer_size 的值,减少对被驱动表的扫描次数)

那么会出现如下问题

1)可能会多次扫描被驱动表,占用磁盘 IO 资源;

2)判断 join 条件需要执行 M*N 次对比(M、N 分别是两张表的行数),如果是大表就会占用非常多的 CPU 资源;

3)可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率。

BKA解决了这个问题

需要设置SET global optimizer_switch=mrr=on,mrr_cost_based=off,batched_key_access=on;

当被join的表能够使用索引时,就先排好顺序,然后再去检索被join的表,因为索引是有序的,我们认为对磁盘的读如果是顺序读,能够提升读性能。相当于减少了磁盘随机IO

听起来和MRR类似,实际上MRR也可以想象成二级索引和primary key的join

如果被Join的表上没有索引,则使用老版本的BNL策略。

实战篇:MySQL优化系列--SQL优化实战_SQL优化_20



MRR是什么

mrr通常指辅助索引回表优化

举个例子

select * from t1 where a>=1 and a<=100; (id是主键,a是辅助索引,表中有 id ,a,b列)

如果随着 a 的值递增顺序查询的话,id 的值就变成随机的,那么就会出现随机访问,
性能相对较差。虽然“按行查”这个机制不能改,但是调整查询的顺序,还是能够加速的。

因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,
如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。

1、根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 ;
2、将 read_rnd_buffer 中的 id 进行递增排序;
3、排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。
这里,read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。
如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 23
然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。

MRR的作用

MRR 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能。

set global optimizer_switch=mrr=on,mrr_cost_based=off;

1. 减少回表

2. 减少随机IO

3. 减少IOPS

4. 减少IO量

实战篇:MySQL优化系列--SQL优化实战_SQL优化_21

索引本身就是为了减少磁盘 IO,加快查询,而 MRR,则是把索引减少磁盘 IO 的作用,进一步放大。

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_22


子查询

(1)派生表

放在from子句后面的子查询称为派生表。

(2)物化

将子查询结果集中的记录保存到临时表的过程,称为物化materialize。

(3)派生表物化

处理派生表时mysql优先尝试把派生表和外层查询进行合并,如果不行,将派生表物化,派生表的结果写入到一个内部的临时表,然后把这个物化表当做普通表一样来参与查询。


(4)最常用IN子查询的原理

IN子查询的问题:
如果子查询结果集中的记录很小,那么把子查询和外层查询分别看成两个单独的单表查询,效率也很高。但是如果子查询结果集大,IN子句参数很多会导致
1)不相关的In子查询如果子查询的结果集太多会导致内存放不下。
2)无法有效的使用索引,只能对外层查询进行全表扫描。
3)在对外层查询执行全表扫描时,如果IN子句中的参数太多,会导致在检测一条记录的In表达式是否为TRUE时花费太多的时间。

通过物化转连接的优化
于是,不直接将不相关子查询的结果集作为外层查询的参数,而是将该结果集写入一个临时表中。
该临时表的列就是子查询结果集中的列,写入临时表的记录会被去重。该临时表基于MEMORY存储引擎,并建立hash索引。
如果查询结果集非常大,超过了系统变量tmp_table_size或者max_heap_table_size的值,临时表会转而使用基于磁盘的存储引擎来保存结果集中的记录。

如果IN子查询不满足转换为半连接的条件,又不能转换为物化表,或者转换为物化表的成本太高,那么它就会被转为EXISTS子查询


举例说明

​https://dev.mysql.com/doc/index-other.html​​ 下测试库表和数据,source到测试mysql中

实战篇:MySQL优化系列--SQL优化实战_优化案例_23


子查询优化

查询sandra的所有电影

select title ,release_year,length from film where film_id in (
select film_id from film_actor where actor_id in ( select actor_id from actor where first_name =sandra));

实战篇:MySQL优化系列--SQL优化实战_优化案例_24

从执行计划看

(1)第1行和第2行,id都显示1,表明查询优化器将子查询转换为了连接查询,且第一行是驱动表,第二行是被驱动表

(2)MATERIALIZED表明优化器将子查询物化之后与外层查询进行连接查询,第1行的table列<subquery2>表示的是执行计划id为2对应的子查询执行之后产生的物化表。

(3)第3行,第4行id都是2,表明子查询转换为了连接查询,且actor表是驱动表。

(4)从type分析扫描方式,第三行actor表进行了全表扫描,且actor和film_actor分别物化后进行连接查询后的结果集也成为了驱动表,并且是全表扫。而被驱动表film的连接条件是主键,所以扫描方式为eq_ref。


将子查询改写为join进行优化

select title ,release_year,length from film f 
join film_actor fa on fa.film_id =f.film_id
join actor a on fa.actor_id=a.actor_idwhere a.first_name=sandra;

查看执行计划

实战篇:MySQL优化系列--SQL优化实战_SQL优化_25

(1)可以看到执行计划id全部都是1,select_type都变成SIMPLE的简单查询,减少了物化的成本,直接进行连接查询。

(2)第2行第3行的扫描方式ref和eq_ref都是成本很低的扫描方式。只有第1行作为驱动表进行的全表扫,只需要将actor表查询列加上索引,就能进一步优化。如下:

实战篇:MySQL优化系列--SQL优化实战_SQL优化_26


group优化

每个演员参与电影数量统计

desc select a.first_name,a.last_name ,count(*) from film_actor fa inner join actor a where a.actor_id= fa.actor_id group by fa.actor_id;

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_27

(1)从执行计划看出第一行Extra中使用了Using temporary; Using filesort表名使用了临时表和排序,为啥group by包含Using filesort,因为mysql在group by中是默认添加order by子句的。

使用临时表的创建和维护的成本很高,应该尽量避免。


执行流程如下:

1、创建内存临时表,表里有三个字段 first_name,last_name , actor_id, cnt(统计数量),主键是actor_id。

2、扫描表actor 的索引 actor_id,依次取出叶子节点上的 actor_id 值。

3、用actor_id去扫描film_actor表,如果满足

a.actor_id= fa.actor_id,就往临时表中写入插入一行( actor_id, first_name,last_name ,1)

如果表中有主键为 actor_id 的行,就将 actor_id 这一行的 cnt 值加 1。

4、遍历完成后,再根据字段actor_id 做排序,得到结果集返回给客户端。


第一次优化:将排序去掉

前面说了group by中是默认添加order by子句的,如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null

desc select a.first_name,a.last_name ,count(*) from film_actor fa inner join actor a where a.actor_id= fa.actor_id group by fa.actor_id order by null;

实战篇:MySQL优化系列--SQL优化实战_SQL优化_28



第二次优化:通过建索引

desc select a.first_name,a.last_name ,c.cnt from actor a inner join
(select actor_id ,count(*) as cnt from film_actor group by actor_id)as c where c.actor_id=a.actor_id;

实战篇:MySQL优化系列--SQL优化实战_优化案例_29


执行流程如下:

将film_actor表actor_id加上索引,按照actor_id先按分组统计出来,就不用在临时表上进行actor_id再累加了,且是有序的。

select actor_id ,count(*) as cnt from film_actor group by actor_id;

那么计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加。

也就是下面这个过程:

当碰到第一个 actor_id 的时候,已经知道累积了 X 个电影,结果集里的第一行就是 ( actor_id, first_name,last_name ,X);

当碰到第二个 actor_id 的时候,已经知道累积了 Y 个电影,结果集里的第二行就是 ( actor_id, first_name,last_name ,Y);

按照这个逻辑执行的话,扫描到整个输入的数据结束,就可以拿到 group by 的结果,不需要临时表,也不需要再额外排序。

最后再将作为派生物化表与actor表连接查询,如下

​(1)第3行显示select_type表示该包含派生表的查询中,是以物化派生表的方式进行查询的。​

​(2)第2行table列显示,表示查询是针对派生物化表之后,与a进行了连接查询。​

先对需要分组的表的列建索引,目的是,通过索引查询出来的其本身有序。按照这个逻辑执行的话,扫描到整个输入的数据结束,就可以拿到 group by 的结果,不需要临时表,也不需要再额外排序。

再与外部关联查询,执行计划没有出现Using temporary; Using filesort了。


group by的执行逻辑

select a from t group by a order by null;
select distinct a from t;
此两类sql执行流程一样:
(1)创建一个临时表,临时表有一个字段 a,并且在这个字段 a 上创建一个唯一索引;
(2)遍历表 t,依次取数据插入临时表中:如果发现唯一键冲突,就跳过;否则插入成功;
(3)遍历完成后,将临时表作为结果集返回给客户端。



group by 的优化参考如下:

(1)如果对 group by 语句的结果没有排序要求,由于group by默认是添加order by子句的,要在语句后面加 order by null,避免排序的消耗;

(2)尽量让 group by 过程用上表的索引,因为索引有序,统计不需要临时表。确认方法是 explain 结果里没有 Using temporary 和 Using filesort;

(3)如果 group by 需要统计的数据量不大,会使用内存临时表,如果 tmp_table_size(控制内存临时表大小) 参数不够大,来使用到磁盘临时表;

(4)如果数据量实在太大,必须要使用到磁盘,使用 SQL_BIG_RESULT 这个提示,来告诉优化器不使用B+数存储,而是使用存储效率更高的数组直接使用排序算法得到 group by 的结果。

        注:直接排序算法也是先用sort_buffer,如果 sort_buffer 内存不够用,就会利用磁盘临时文件辅助排序;


使用到临时表的逻辑

​如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果;​

​join_buffer 是无序数组,sort_buffer 是有序数组,临时表是二维表结构;​

​如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。union 需要用到唯一索引约束, group by 还需要用到另外一个字段来存累积计数。​


limit优化

limit常用于分页处理,时常会伴随order_by从句使用,因此大多时候会使用filesort这样会造成大量IO问题。

select film_id,description from film order by title limit 50,5;

实战篇:MySQL优化系列--SQL优化实战_执行计划_30

进行了全表扫描

改写为根据主键或者索引排序,扫描方式优化为index

select film_id,description from film order by film_id limit 50,5;

实战篇:MySQL优化系列--SQL优化实战_优化案例_31

进一步优化扫描范围太大问题,根据主键进行范围查询,进一步优化为range

select film_id,description from film where film_id >55 and film_id<=60 order by film_id limit 1,5;

实战篇:MySQL优化系列--SQL优化实战_MySQL优化_32


in和exists的优化

之前有发个《IN和EXISTS谁效率更高》可以回顾下

这里再讲一下原理

IN原理

在in()的执行中,是先执行内表得到结果集,再执行外表,外表会对所有的内表结果集匹配,也就是如果外表有100,内表有10000,就会执行100*10000次,所以在内表比较大的时候,不合适用in()方法,效率比较低。

select * from 外表 a where id i n(select 相关id from 内表) in的执行类似如下:

List resultSet=[];
Array A=(select * from A);
Array B=(select id from B);

for(int i=0;i<A.length;i++)
for(int j=0;j<B.length;j++)
if(A[i].id==B[j].id)
resultSet.add(A[i]);
break;



return resultSet;

exists原理

exists ()的执行过程中,并没有对每一条内表的数据都进行查询,而是存在该条数据的时候会将结果集存起来,到最后的时候同一输出结果集。

select a.* from 外表 a where exists(select 1 from 内表 b where a.id=b.id) 的exists的执行语句如下:

List resultSet=[];
Array A=(select * from 外表 A)

for(int i=0;i<A.length;i++)
if(exists(A[i].id) //执行select 1 from 内表 b where b.id=a.id是否有记录返回
resultSet.add(A[i]);


return resultSet;

设:外表A,内表B。

A表有10000条记录,B表有1000000条记录, 那么exists()会执行10000次去判断A表中的id是否与B表中的id相等。

A表有10000条记录,B表有100000000条记录,那么exists()还是执行10000次,因为它只执行A.length次,可见B表数据越多,越适合exists()发挥效果。

再如:A表有10000条记录,B表有100条记录,那么exists()还是执行10000次,还不如使用in()遍历10000*100次,因为in()是在内存里遍历比较,而exists()需要查询数据库,我们都知道查询数据库所消耗的性能更高,而内存比较很快。


结论

1、IN查询在内部表和外部表上都可以使用到索引;

2、Exists查询仅内部表上可以使用到索引,外表会全表扫描;当子

以上是关于实战篇:MySQL优化系列--SQL优化实战的主要内容,如果未能解决你的问题,请参考以下文章

Flutter上线项目实战——性能优化篇(未完待续)

Flutter上线项目实战——性能优化篇(未完待续)

第24天SQL进阶-查询优化- performance_schema系列实战一:利用等待事件排查MySQL性能问题(SQL 小虚竹)

第24天SQL进阶-查询优化- performance_schema系列实战一:利用等待事件排查MySQL性能问题(SQL 小虚竹)

第24天SQL进阶-查询优化- performance_schema系列实战一:利用等待事件排查MySQL性能问题(SQL 小虚竹)

第24天SQL进阶-查询优化- performance_schema系列实战一:利用等待事件排查MySQL性能问题(SQL 小虚竹)