join 查询优化

在开发中每每会出现查询多表联查的状况,那么就会用到 join 查询。html

Join查询种类

为了方便说明,先定义一个统一的表,下面再作例子。mysql

CREATE TABLE `t2` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB;

表 t一、t2 结构相等,t2 1000条记录, t1 100 条记录,t1 的数据在 t2 上都有。算法

Index Nested-Loop Join(NLJ)

关于 NLJ 能够从名字上直接看出,这是一个使用到索引的 join 查询,正由于使用到索引,因此不须要用到临时表。sql

例子

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

straight_join :至关于强制版的 inner join。由于在使用 inner join 或 join 链接时,优化器默认使用的都是小表做为驱动表,可是若是排序或筛选条件列是被驱动表,那么该列的索引就会用不到,好比 select * from a inner join b where a.id=b.aid order by a.id,若是a的集合比b大,那么mysql就会以b为驱动表,这个时候若是a.id有索引的话,那么这个索引在 order by 排序时是不起效的(on 筛选时能够用到,排序只能经过驱动表排序那么就须要额外排序,甚至用到临时表,很是消耗性能,而 straight_join 的做用就是强制让左表做为驱动表,固然使用前提是在事先知道优化器选择的驱动表效率较低,而后才能够考虑使用 straight_join 。这里就是为了防止优化器选择错误的驱动表,固然,这里使用 inner join 也是会以 t1 小表做为基础表。oop

执行过程性能

一、从表 t1 中读入一行数据 R;
二、从数据行 R 中,取出 a 字段到表 t2 里去查找;
三、取出表 t2 中知足条件的行,跟 R 组成一行,做为结果集的一部分;
四、重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。优化

整个过程步骤1 会遍历 t1 全部的行,因此一共会扫描100行。 而步骤2 由于会用到索引,因此每次扫描一次,一共扫描 100 行。最后拼接在一块儿,因此一共扫描 200 行。spa

公式线程

假设被驱动表的行数是 M。每次在被驱动表查一行数据,要先搜索索引 a,再搜索主键索引。每次搜索一棵树近似复杂度是以 2 为底的 M 的对数,记为 log2M,因此在被驱动表上查一行的时间复杂度是 2*log2M。
假设驱动表的行数是 N,执行过程就要扫描驱动表 N 行,而后对于每一行,到被驱动表上匹配一次。所以整个执行过程,近似复杂度是 N + N*2*log2M。

由此看来,不考虑其余因素的影响(好比上面straight_join 说到的状况),NLJ 方式以小表做为驱动表的效率会更高

 

Simple Nested-Loop Join

没有用到索引。

例子

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

执行过程

首先由于 straight_join 的做用,仍是以 t1 为驱动表。执行时仍是先从 t1 上取一条记录取 t2 上寻找对应的记录,可是由于 t2 的 b 列上没有索引,因此在 t2 上执行的是全表扫描。因此扫描行数为 100+100*1000。因此 Simple Nested-Loop Join 的效率很低,这种方式也是没有被 MySQL 采用。

 

Block Nested-Loop Join(BNJ)

这种方式也是没有用到索引,可是和上面一种的区别是在内存中实现的。主要思路是将驱动表加载到一个内存空间 join_buffer 中,而后从被驱动表上每次拿出一条记录到 join_buffer 中找到符合条件的记录,而后返回。join_buffer 大小可由参数 join_buffer_size 设定,默认值是 256k。在 explain 中出现 Block Nested Loop 说明使用的是 BNJ 算法,BNJ 执行效率比较低,因此应该避免。 

例子

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

执行过程

一、若是 join_buffer 空间足够存储 t1 全部记录:

  1)把表 t1 的数据读入线程内存 join_buffer 中,因为咱们这个语句中写的是 select *,所以是把整个表 t1 放入了内存;
  2)扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据作对比,知足 join 条件的,做为结果集的一部分返回。须要注意的是,由于 join_buffer 是无序数组,因此虽然 t1 的a 列有索引,在这一步寻找时也不会用到索引

 

二、若是 join_buffer 空间不能存储 t1 的全部记录。那么就会分批来处理。

  1)扫描表 t1,顺序读取数据行放入 join_buffer 中,放完第 88 行 join_buffer 满了,继续第 2 步;
  2)扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据作对比,知足 join 条件的,做为结果集的一部分返回;
  3)清空 join_buffer;
  4)继续扫描表 t1,顺序读取最后的 12 行数据放入 join_buffer 中,继续执行第 2 步。

 

结论:以上这两种扫描的总行数都是同样的。 一、将 t1扫描进 join_buffer 100行;二、t2 每行去 joiin_buffer 上进行全表扫描 100*t2总行数1000。因此总行数为 100 +100*1000。和上面的 Simple Nested-Loop Join 方式扫描的行数同样,可是由于使用的 join_buffer 是在内存中的,因此执行的速度会比 Simple Nested-Loop Join 快得多。

公式

假设,驱动表的数据行数是 N,须要分 K 段才能完成算法流程,被驱动表的数据行数是 M。注意,这里的 K 不是常数,N 越大 K 就会越大,所以把 K 表示为λ*N,显然λ的取值范围是 (0,1)。那么扫描行数就是 N+λ*N*M。

由此能够看出,驱动表参与筛选的记录数越少,扫描的行数就越少,效率也就越高。也就是在不考虑其余因素的影响下,以小表为驱动表能够提升 BNJ方式的执行效率

 

优化

Index Nested-Loop Join(NLJ)

NLJ 查询过程当中会用到索引,因此查询的效率会很快,可是其仍是有优化空间的,那就是 MySQL 5.6引入的 Batched Key Access(BKA) 算法。其原理就是经过 MRR 实现顺序读,由于以前的 NLJ 过程是每次拿一条记录去匹配,而后获得对应的一条记录,这样每次获取的记录主键颇有可能不是按顺序去查询的,同时屡次的查询使得执行效率比较低(每次都须要从 B+树的根节点开始查找匹配)。

MRR

MRR 会先将要查询的记录主键添加到 read_rnd_buffer中(若是放不下就分屡次进行),对 read_rnd_buffer 中的 id 进行递增排序,而后再依次按 id 去查询,通过 MRR 优化的执行就会走 B+ 树的叶子节点,因此查询效率提升。下面以 sql:select * from t1 where a>=1 and a<=100 为例,其中 a 列有索引,看一下开启 MRR 执行的流程图:

开启

设置:SET  @@optimizer_switch='mrr=on,mrr_cost_based=on';

相关参数:
当mrr=on,mrr_cost_based=on,则表示cost base的方式还选择启用MRR优化,当发现优化后的代价太高时就会不使用该项优化
当mrr=on,mrr_cost_based=off(官方文档的说法,是如今的优化器策略,判断消耗的时候,会更倾向于不使用 MRR,把 mrr_cost_based 设置为 off,就是固定使用 MRR 了。),则表示老是开启MRR优化。

 

若是查询使用了 MRR 优化,那么使用 explain 解析就会出现 Using MRR 的提示

BKA

使用 BKA 的过程:

仍是以上面的 select * from t1 straight_join t2 on (t1.a=t2.b); 为例

首先将 t1 的筛选字段存入 join_buffer(若是存不下就分屡次执行),而后将存储的字段值批量去 t2 上匹配,获得匹配的主键,而后进行主键排序,再去依次查询对应的记录返回。

 

Block Nested-Loop Join(BNJ)

BNJ 形成性能损失很高,主要缘由有如下三个方面:

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

二、判断 join 条件须要执行 M*N 次对比(M、N 分别是两张表的行数),若是是大表就会占用很是多的 CPU 资源;

三、可能会致使 Buffer Pool 的热数据被淘汰,影响内存命中率(影响严重)。经过 InnoDB 中的缓冲池(Buffer Pool) 能够知道缓冲池是使用了 LRU 算法来对热点数据进行了优化的,可是在某些状况下仍是会出现热点数据被挤掉的场景,使用 BNJ 进行屡次的查询就是其中一种,由于 BNJ 操做若是涉及的表数据量比较大,那么用到的数据也不少,那么若是在使用到后面某一时刻某个会话也查询了某个冷门数据,那么由于以前 BNJ 也查询了,而且中间的时间间隔达到了最大老年时间,因此这个冷门数据就会进入老年代头部,挤掉其余热点数据。大表 join 操做虽然对 IO 有影响,可是在语句执行结束后,对 IO 的影响也就结束了。可是,对 Buffer Pool 的影响就是持续性的,须要依靠后续的查询请求慢慢恢复内存命中率。

优化思路1:减小 BNJ 的循环次数,上面说到,屡次的扫描被驱动表会长时间占用磁盘 IO 资源,形成系统总体性能降低。

方法:增大 join_buffer_size 的值,减小对被驱动表的扫描次数。

优化思路2:将 BNJ 优化成 NLJ。

方法1:在筛选条件字段使用率比较高时,能够考虑为其建立一个索引,这样在执行时由于有索引就会变成 NLJ 了。

方法2:若是筛选字段使用率很低,为其建立索引会提升维护的成本,作到得不偿失,那么该如何优化?答案是可使用临时表,从 MySQL 中的临时表 能够知道,临时表会随着会话的结束而自动销毁,省去了维护的成本;同时不一样会话能够建立同名的临时表,不会产生冲突。这使得临时表成为优化筛选字段使用率低的 BNJ 查询的绝佳方法。

  例:假设有表 t一、t2,表 t1 里,插入了 1000 行数据, t2 中插入了 100 万行数据 。执行 select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000; b 列使用率很低

  未优化前执行

  一、把表 t1 的全部字段取出来,存入 join_buffer 中。这个表只有 1000 行,join_buffer_size 默认值是 256k,能够彻底存入。
  二、扫描表 t2,取出每一行数据跟 join_buffer 中的数据进行对比,
    1)若是不知足 t1.b=t2.b,则跳过;
    2)若是知足 t1.b=t2.b, 再判断其余条件,也就是是否知足 t2.b 处于[1,2000]的条件,若是是,就做为结果集的一部分返回,不然跳过。整个筛选过程一共扫描了 1000*1000000 = 10亿行。

  优化思路

  一、把表 t2 中知足条件的数据放在临时表 tmp_t 中;
  二、为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;
  三、让表 t1 和 tmp_t 作 join 操做。

  实现

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

  过程消耗:

  一、执行 insert 语句构造 temp_t 表并插入数据的过程当中,对表 t2 作了全表扫描,这里扫描行数是 100 万。
  二、以后的 join 语句,扫描表 t1,这里的扫描行数是 1000;join 比较过程当中,作了 1000 次带索引的查询(由于t1 1000行,做为驱动表,t2做为被驱动表)。相比于优化前的 join 语句须要作 10 亿次条件判断来讲,这个优化效果仍是很明显的。

  进一步优化

  临时表又分为磁盘临时表和内存临时表,使用内存临时表效率比磁盘临时表高,上面的引擎是 innodb,也就是磁盘临时表,若是换成 Memory 引擎就是内存临时表。可是相对的内存临时表只能存储2000行数据,因此在数据量特别大时仍是应该使用磁盘临时表。

三张表优化

表结构:

CREATE TABLE `t1` (
 `id` int(11) NOT NULL,
 `a` int(11) DEFAULT NULL,
 `b` int(11) DEFAULT NULL,
 `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
create table t2 like t1;
create table t3 like t2;
insert into ... //初始化三张表的数据

如何优化语句:select * from t1 join t2 on(t1.a=t2.a) join t3 on (t2.b=t3.b) where t1.c>=X and t2.c>=Y and t3.c>=Z;

答:首先根据where 三个条件来判断哪一个表符合条件的返回的字段长度最小,将最小的做为驱动表。
一、第一种状况,若是选出来是表 t1 或者 t3,那剩下的部分就固定了。(由于 join 顺序是 t一、t二、t3,肯定小表直接向另外一个方向驱动就能够了)
  1)若是驱动表是 t1,则链接顺序是 t1->t2->t3,要在被驱动表字段建立上索引,也就是 t2.a 和 t3.b 上建立索引;
  2)若是驱动表是 t3,则链接顺序是 t3->t2->t1,须要在 t2.b 和 t1.a 上建立索引。
同时,咱们还须要在第一个驱动表的字段 c 上建立索引。
二、第二种状况是,若是选出来的第一个驱动表是表 t2 的话,则须要评估另外两个条件的过滤效果。

 

总之,总体的思路就是,尽可能让每一次参与 join 的驱动表的数据集,越小越好,由于这样咱们的驱动表就会越小。

 

总结

NLJ 本来是不须要用到 join_buffer 的,可是能够经过 BKA 优化使用 join_buffer ,此时方向是使用在 join_buffer 中的驱动表数据去被驱动表上匹配,而后获得主键,排序、回表返回结果,若是 read_rnd_buffer 或者 join_buffer 空间不够就分屡次进行。

BNL 本来没有用到索引,因此必须使用 join_buffer 来帮助查询,方向是被驱动表到 join_buffer 上的驱动表数据进行匹配,优化后变成 BKA 算法的 NLJ,因此方向也就变成了使用在 join_buffer 中的驱动表数据去被驱动表上匹配。因此在 BNL 优化前的思路就是减小被驱动表的遍历次数,也就是增大 join_buffer 的大小;而优化后就须要在被驱动表上建立索引,来优化查询。

 

join 的 on 条件与 where 条件的关联

表结构:

create table a(f1 int, f2 int, index(f1))engine=innodb;
create table b(f1 int, f2 int)engine=innodb;
insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
insert into b values(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);

on 条件写在 where 中可能会使外链接失效

以上面的表结构,执行:

select * from a left join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q1*/
select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/

执行结果:

分析

Q1:

解析:

由于是以 a 做为驱动表,而 a 的 f1有索引,f2没有索引,因此会用到临时表来筛选,也就出现 using join buffer(Block hested Loop)

过程

一、把表 a 的内容读入 join_buffer 中。由于是 select * ,因此字段 f1 和 f2 都被放入 join_buffer 了。
二、顺序扫描表 b,对于每一行数据,判断 join 条件(也就是 (a.f1=b.f1) and (a.f1=1))是否知足,知足条件的记录, 做为结果集的一行返回。若是语句中有 where 子句,须要先判断 where 部分知足条件后,再返回。
三、表 b 扫描完成后,对于没有被匹配的表 a 的行(在这个例子中就是 (1,1)、(2,2) 这两行),把剩余字段补上 NULL,再放入结果集中。

 

Q2:

解析:

为何Q2执行会把 null 值部分过滤掉了?

这是由于在 where 条件中,NULL 跟任何值执行等值判断和不等值判断的结果,都是 NULL。这里包括, select NULL = NULL 的结果,也是返回 NULL。因此在筛选时,先经过 on 判断带 null 的记录,可是由于 where 条件的做用,会筛掉其中为 null 的记录,致使 left join 失效,因此优化器在实际执行时会将这条语句优化成 inner join,并把筛选条件移到 where 条件后面。整个语句就会被优化成下面的语句执行:

也就是 select a.f1, a.f2, b.f1, b.f2 from  a  join  b  where a.f1 = b.f1 and a.f2=b.f2

因此过程就变成:
顺序扫描表 b,每一行用 b.f1 到表 a 中去查,匹配到记录后判断 a.f2=b.f2 是否知足(索引下推),知足条件的话就做为结果集的一部分返回。

 

因此,若是想要执行外链接查询,筛选条件就不能写在 where 中。

 

内链接可能会将on条件优化成 where 条件

执行:

select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q3*/
select * from a join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q4*/

解析:

能够看到两条语句解析结果是如出一辙的,而且执行语句也与这两条语句都不同,都被优化成 select * from a join b where a.f1=b.f1 and a.f2=b.f2 。执行过程和上面的 Q2 同样。这是由于若是放在 on 中就会用到临时表,效率会低一些,因此优化器直接优化放在 where 中配合索引下推经过索引一并完成判断。

相关文章
相关标签/搜索