Skip to content

Latest commit

 

History

History
323 lines (178 loc) · 14.9 KB

优化.md

File metadata and controls

323 lines (178 loc) · 14.9 KB

Join

可不可以使用join

为了便于分析执行过程中的性能问题,可以使用straight_join来让MySQL使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去join,比如下面这条语句:

select * from t1 straight_join t2 on (t1.a=t2.a);

那么t1就是驱动表,t2就是被驱动表

其中t1有100行数据,t2有1000行数据,两个表中都是id,a,b三个字段,其中id和a字段均有索引。

join类型

Index Nested-Loop Join

以上面SQL为例,这个查询过程跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以称为"Index Nested-Loop Join",简称NLJ。

这里就有两个问题:

1、可以使用join吗?

如果不使用join,那么就需要单表查询t1,然后遍历t1,再去查询t2,虽然跟直接join相比扫描的行数是一样,但是执行的语句比直接使用join多,而且客户端还要组装数据,那么显示是直接使用join更好。

2、怎么选择驱动表

在这个SQL中,通过explain可以发现,驱动表走的是全表扫描,被驱动表走的是树搜索。

假设被驱动表的行数是M,每次在被驱动表查一行数据,要先搜索索引a,再搜索主键索引。每次搜索一棵树近似复杂度是以2为底的M的底数,记为log2M,所以在被驱动表上查一行的时间复杂度是2*log2M。

假设驱动表的行数是N,执行过程就要扫描驱动表N行,然后对于每一行,到被驱动表上匹配一次。

因此整个执行过程中,近似复杂度是N+N*2*log2M显然,N对扫描行数的影响更大,因此应该让小表来做驱动表

结论

1、使用join语句,性能比强行拆成多个单表执行SQL语句的性能更好

2、如果使用join语句的话,需要让小表做驱动表

但是,需要主要,这个结论的前提是可以使用被驱动表的索引

Simple Nested-Loop Join

这种类型的join就是被驱动表用不上索引

比如,将上面的SQL改成这样:

select * from t1 straight_join t2 on (t1.a=t2.b);

因为t2的字段b上没有索引,因此每次到t2去匹配的时候,都要做一次全表扫描。

显然,这个算法太笨重了,MySQL没有使用这个算法,而是使用了下面这种算法

Block Nested-Loop Join

算法流程如下:

1、把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存

2、扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回

在这个过程中,对表t1和t2都做了一次全表扫描,因此总的扫描行数是1100。由于join_buffer是以无序数组的方式组织的,因此对表t2中的每一行,都要做100次判断,总共需要在内存中做的判断次数是:100*1000=10万次。

这个扫描行数和上一个算法是一样的,但是,BNJ算法的这10万行次判断是内存操作,速度上会快很多,性能也更好。

在这种情况下,应该选择哪个表做驱动表呢?

假设小表的行数是N,大表的行数是M,那么在这个算法里:

1、两个表都做一次全表扫描,所以总的扫描行数是M+N

2、内存中的判断次数是M*N

可以发现,调换M和N的顺序没什么差别,所以这时候选择大表还是小表做驱动表,执行耗时是一样的。

但是要注意,这里的表t1才100行,如果t1是一个大表,join_buffer放不下怎么办呢

Join_buffer的大小是由参数join_buffer_size设定的,默认值是256k。如果放不下表t1的所有数据的话,策略很简单,就是分段放。这也是该算法名字中"Block"的由来,表示"分块去join"。

这种情况下驱动表该如何选择呢?这里说下结论:

还是应该让小表当驱动表

并且join_buffer_size越大,一次可以放入的行越多,分成的段数也就越少,对被驱动表的全表扫描次数就越少。

所以有的建议说,如果你的join语句很慢,就把join_buffer_size改大

结论

1、能不能使用join语句?

  • 如果可以使用NLJ算法,也就是说可以用上被驱动表的索引,其实是没问题的
  • 如果使用BNJ算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表多次,占用大量系统资源,所以这种join尽量不要用。

所以在判断要不要使用join语句时,就是看explain结果里,Extra字段里面有没有出现"Block Nested Loop"字样。

2、如果要使用join,应该选择大表做驱动表还是小表做驱动表?

  • 如果是NLJ算法,选择小表做驱动表
  • 如果是BNJ算法:
    • join_buffer_size足够大的时候,是一样的
    • join_buffer_size不够大的时候(这种情况更常见),应该选择小表做驱动表

所以,结论就是总是应该使用小表做驱动表

这里需要说明下,什么叫做小表

比如下面这两条语句:

select * from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=50;
select * from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=50;

都没用上索引,但第二个语句,join_buffer只需要放入t2的前50行,所以这里,"t2的前50行"就是那个相对小的表,也就是"小表"。

再来看另外一个例子:

select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100;
select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100;

都是只有100行参加join,但是两条语句每次查询放入join_buffer中的数据是不一样的

  • 表t1只查字段b,因此把t1放到join_buffer中,join_buffer只需放入b的值
  • 表t2需要查所有的字段,因此如果把t2放到join_buffer的话,就需要放入三个字段id,a和b

这里应该选择t1作为驱动表,在这个例子里,"只需要一列参与join的表t1"是那个相对小的表。

所以,更准确的说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是"小表",应该作为驱动表

join怎么优化

Multi-Range Read优化

即MRR,这个优化的主要目的是尽量使用顺序读盘。

还记得“回表”这个概念吗?即在普通索引上查到主键值后,再根据一个个主键的值到主键索引上去查整行数据的过程。

那回表过程是一行行地查数据,还是批量地查数据?

比如下面这条语句:

select * from t1 where a>=1 and a<=100;

主键索引是一棵B+树,在这棵树上,每次只能根据一个主键ID查到一行数据。因此,回表肯定是一行行搜索主键索引的

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

MRR能够提升性能的核心在于,上面这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键ID。这样通过排序以后,再去主键索引查数据,才能体现出"顺序性"的优势。

Batched Key Access

BKA算法,其实就是对NLJ算法的优化。

NLJ算法执行的逻辑是:从驱动表t1,一行行地取出a的值,再到被驱动表t2去做join。也就是说,对于表t2来说,每次都是匹配一个值。这时,MRR的优势就用不上了。

那怎么才能一次性地多传些值给表t2呢?方法就是,从表t1里一次性地多拿些出来,一起传给表t2。

既然如此,我们就把表t1的数据取出来一部分,先放到一个临时内存。这个临时内存不是别人,就是join_buffer。

如果要使用BKA优化算法的话,需要在执行SQL语句之前,设置:

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

前两个参数的作用是启用MRR,这么做的原因是BKA算法要依赖于MRR。

前面我们说过BNL算法,它会对系统造成下面三个方面的影响:

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

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

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

所以我们在执行语句之前,需要通过分析和查看explain结果的方式,确认是否要使用BNL算法,如果确认优化器会使用BNL算法,就需要做优化。优化的常见做法是,给被驱动表的join字段加上索引,把BNL算法转成BKA算法

BNL转BKA

一些情况下,可以直接在被驱动表上建索引,这时就可以直接转成BKA了。

但是,如果是一个低频的SQL语句,那么再为这个语句创建索引就很浪费了。

那有没有两全其美的办法呢?

这时可以考虑使用临时表,比如下面这条SQL:

select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;

t1表有1000条数据,t2表有100万数据。

思路是:

1、把表t2中满足条件的数据放在tmp_t中

2、为了让join使用BKA算法,给临时表tmp_t的字段b加上索引

3、让表t1和tmp_t做join操作

对应的SQL写法如下:

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

总体来看,不论是在原表上加索引,还是用有索引的临时表,思路都是让join语句能够用上被驱动表的索引,来触发BKA算法,提升查询性能

扩展hash join

其实上面那个语句,会进行1000*100万=10亿次操作,如果join_buffer里面维护的不是一个无序数组,而是一个哈希表的话,那么就不是10亿次判断,而是100万次hash查找。但是MySQL不支持哈希join。但是我们可以在业务端实现:

1、select * from t1,取得表t1的全部1000行数据,在业务端存入hash结构

2、select * from t2 where b>=1 and b<=2000,获取表t2中满足条件的2000行数据

3、把这2000行数据,一行一行地取到业务端,到hash结构的数据表中寻找匹配的数据,满足匹配的条件的这行数据,就作为结果集的一行

理论上,这个过程会比临时表方案的执行速度要快一些。

结论

NLJ的优化就是MRR算法,而BNL的优化就是BKA算法。

在这些优化方法中:

1、BKA优化是MySQL已经内置支持的,建议默认嗨哟个

2、BNL算法效率低,建议转成BKA算法。优化方向就是给被驱动表的关联字段加上索引

3、基于临时表的改进方案,对于能够提前过滤出小数据的join语句来说,效果还不错

4、由于MySQL还不支持hash join,所以可以配合应用端,理论上效果好于临时表方案。

临时表

临时表分为内存临时表和磁盘临时表,如果内存临时表放不下,就会使用磁盘临时表。

union

现在有一个表t1,id是主键,a是索引,还有一个字段b,id从1000开始逆序存储

执行下面这条语句:

(select 1000 as f) union (select id from t1 order by id desc limit 2);

这条语句用到了union,它的语义是,取这两个子查询结果的并集。并集的意思就是这两个集合加起来,重复的行只保留一行。

这个语句的执行流程如下:

1、创建一个内存临时表,这个临时表只有一个整型字段f,并且f是主键字段

2、执行第一个子查询,得到1000这个值,并存入临时表中

3、执行第二个子查询

  • 拿到第一行id=1000,试图插入临时表中。但由于1000已经存在,违反唯一性约束,所以插入失败,然后继续执行
  • 取第二行id=999,插入临时表成功

4、从临时表中按行取出数据,返回结果,并删除临时表,结果表中包含两行数据分别是1000和999。

所以这里的临时表起到了暂存数据的作用,而且计算过程还用上了临时表主键ID的唯一性约束,实现了union的语义。

顺便提一下,如果把union改成union all,就没有了去重的语义。这样执行的时候,就依次执行子查询,得到的结果直接作为结果集的一部分,发给客户端,也就不需要临时表了。

group by优化

1、如果对group by语句的结果没有排序要求,要在语句后面加上order by null,不是直接去掉order by,而是要显示加上order by null,这是规定。

注:在mysql8.0开始,不需要显示添加order by null了,因为从这个版本开始,group by会进行隐式排序的问题已经被抛弃了。所以具体得看当前所使用的数据库版本

2、尽量让group by过程用上表的索引,确认方法是explain结果里没有Using temporaryUsing filesort

3、如果group by需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大 tmp_table_size参数,来避免用到磁盘临时表;

4、如果数据量实在太大,使用SQL_BIG_RESULT这个提示,来告诉优化器直接使用排序算法 得到group by的结果。因为你如果一个group by语句中需要放到临时表上的数据量特别大,却还是要按照"先放到内存临时表,插入一部分数据后,发现内存临时表不够用了再转成磁盘临时表",看上去就有点儿傻。

其他

1、删除数据的时候尽量加limit,这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。具体见锁这章。

2、读提交隔离级别下,锁的范围更小,锁的时间更短,因为在执行过程加上的行锁,在语句执行完成后,就要把“不满足条件的行”上的行锁直接释放,不需要等到事务提交。

3、现在在MySQL中有一张表table,有四百多万数据。

执行下面这个语句:

select * from table limit 2000000,2000

时3秒

而执行

select a.* from table a,(select id from table limit 2000000,2000) b where a.id=b.id

只耗时了0.3秒,为什么呢?

这是因为第二个查询语句使用了子查询,而第一个查询语句没有。

在第一个查询语句中,MySQL必须检索整个表中的所有记录,然后跳过前2000000个记录,并返回接下来的2000个记录。这需要MySQL读取和处理所有记录,包括前2000000个记录,这是一个昂贵的操作,因此需要更长的时间。

而在第二个查询语句中,子查询先执行,它只返回所需的2000个id。MySQL只需要在主查询中查找与这些id匹配的记录,这个操作更加快速和有效,因为MySQL只需要读取和处理少量的记录。

因此,使用子查询可以使查询更加有效,特别是当要处理大型表时。