MySQL--join算法(Nested-Loop JoinBlock Nested-Loop Join)

Posted 黄智霖-blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL--join算法(Nested-Loop JoinBlock Nested-Loop Join)相关的知识,希望对你有一定的参考价值。

目录

前言

  mysql的一个连接查询涉及到两张表的关联,既然涉及到两张表的关联那么就需要一定的算法来组织数据。关联的两张表,一个叫做驱动表,一个叫做被驱动表
  现在有了两张表,总要有一张表先被查询,然后以此结果再去关联另一张表。这个第一张表就被称为驱动表,第二张表就称为被驱动表,一般我们都要以查询结果集相对小的那张表作为驱动表。
MySQL中,join分为三类,分别是:

  • inner join(inner可省略):获取两个表中字段匹配关系的记录,也就是等值查询
  • left join:左连接
  • right join:右连接

  对于inner join来说,驱动表的选择需要有一个标准,而通常都是以查询结果集相对较小的那张表作为驱动表。一个表的查询结果集主要是通过where条件预估返回的条数和该表需要返回的字段来进行计算。当然,预估返回条数不会真正去做查询动作,如果条件有索引,那么可以根据索引来预估条数;如果没法用索引,或者直接没有条件,就认为是全表。不过查询优化器最终会基于成本计算来进行评估,像排序、回表等都会对成本有影响,最终SQL如何执行还是要看查询优化器如何抉择。
  比如以下sql(a和b的表结构很简单,这里就不贴了):

explain select * from b join a on a.key = b.key


  id序号相同,上面的先执行,所以这里a表是驱动表,b表是被驱动表。这个是查询优化器选择的,我们也可以使用straight_join来强制指定驱动表(straight_join左边的为驱动表):

explain select * from b straight_join a on a.key = b.key


  可以看到驱动表变成了b表,不过straight_join只适用于inner join的情况,因为left(right)join已经明确了驱动表:left join左边的表是驱动表,right join右边的表是驱动表。

join 算法

  MySQL的关联查询使用的算法叫做Nested-Loop join(循环嵌套)算法,而针对不同的情况,NLJ会有不同的变种:

  • 关联字段有索引:对应Index Nested-Loop joinBatched Key Access join算法
  • 关联字段无索引:Block Nested-Loop join算法

Index Nested-Loop join

  其实也可以简称为Nested-Loop join(NLJ)算法,原理是先根据条件在驱动表中取出一条数据,然后从中取出关联字段到被驱动表中根据索引进行关联,取出满足条件的行,然后根据查询字段合并结果存入最终结果集中,依次循环,就和嵌套for循环一样。

Batched Key Access join

  在上面描述的NLJ算法中有一个问题,如果和被驱动表关联的索引是辅助索引,并且查询字段无法做到索引覆盖,那么在组装数据的时候就需要回表操作。而如果匹配每条记录都去回表,效率肯定不高,虽然回表能够使用到主键索引,但是因为这里id不一定有序,所以也属于随机分散读取。对于这种情况,MySQL提供了一种优化措施,提供了一种叫做Batched Key Access join的算法,即批量主键访问连接算法。该算法的原理是,先在驱动表中根据条件查询出符合条件的记录存入join buffer中,然后根据索引获取被驱动表的索引记录,将其存入read_rnd_buffer中。如果join buffer或read_rnd_buffer有一个满了,那么就先处理buffer中的数据:将read_rnd_buffer中的被驱动表索引记录按照主键进行升序排序,然后依赖这个有序的记录去回表查询,由于主键索引中的记录是按照主键升序排序的,这样能提高回表效率。要启用BKA算法,需要开启batched_key_access。这里测试一下:

set optimizer_switch='batched_key_access=off';
explain select a.*,b.* from b join a on b.key = a.key  


  这里没有开启batched_key_access,可以看到,a是驱动表,b.key字段上有索引,查询字段使用的是a.*,b.*,所需数据需要回表,但是还是使用的是NLJ算法,现在开启batched_key_access试试:

set optimizer_switch='batched_key_access=on';
explain select a.*,b.* from b join a on b.key = a.key


  可以看到这次使用了BKA算法,使用到了join buffer,那么如果我们查询字段不选择b.*,只返回b.id字段,这样能用到覆盖索引:

set optimizer_switch='batched_key_access=on';
explain select a.*,b.id from b join a on b.key = a.key 


  发现没有再使用BKA算法了。

Block Nested Loop Join

  如果被驱动表关联字段没有可用的索引,那么就要使用BNL算法了,这个算法和BKA算法一样需要用到join buffer,但是没有使用read_rnd_buffer。先根据条件查出驱动表中符合条件的记录,存入join buffer中,如果join buffer只能存100条数据,但是驱动表符合条件的就结果集超过100条,那么也只能取到100条,称为一个批次(batch),然后全表扫描被驱动表,将被驱动表的每行记录都和join buffer中的记录进行匹配,将匹配到的记录放到最终结果集中。被驱动表扫描完之后,清空join buffer,再次重复从驱动表中获取剩余记录存入join buffer,然后全表扫描被驱动表,到join buffer中进行匹配,依次循环直到数据匹配完毕。
  现在把b.key字段上的索引删除,再看看查询记录:

explain select a.*,b.id from b join a on b.key = a.key  


  可以看到使用了BNL算法。
  试想一下,在被关联表关联字段没有索引的情况下如果使用NLJ算法效率如何呢(没有索引也无法使用BKA算法)?先查驱动表,找到一行数据,在到被驱动表中去匹配,由于没有索引,所以只能全表扫描寻找数据,这个极其不可控,如果被驱动表的数据量稍微大一点,光这个全表扫描的次数就够吓人了(驱动表结果集中的每行数据都要对应被驱动表的一次全表扫描)。所以使用了BNL算法,先将结果集较少的驱动表的数据放入join buffer,全表扫描被驱动表然后到join buffer中去匹配数据,这个匹配操作就是在内存中处理的,相对来说还能接受。当然,如果join buffer无法一次性装载驱动表的结果集,那么被驱动表也需要重复扫描。

总结

  本文主要介绍了Index Nested-Loop join(NLJ)、Batched Key Access join(BKA)和Block Nested-Loop join(BNL)三个算法,分别针对关联查询能使用索引、能使用索引但是被驱动表需要回表和无法使用索引几种情况。
  在平时使用过程中,left join 和right join的语法就决定了驱动表和被驱动表,但是inner join可以有选择的余地,通常是以小结果集驱动大结果集为标准进行选择,这个默认MySQL查询优化器会为我们做出选择,但是查询优化器的选择标准还是是基于成本计算的,涉及到其它多种因素。如果要强制指定inner join下选择的驱动表,可以使用straight_join,只是通常情况下不需要我们主动抉择。我们作为开发人员,大方向上需要注意的主要就是两点,一是尽量不要有太多的表关联,二是被关联表的关联字段要合理建立索引。

以上是关于MySQL--join算法(Nested-Loop JoinBlock Nested-Loop Join)的主要内容,如果未能解决你的问题,请参考以下文章

Nested-Loop Join Algorithms

MySQL Block Nested-Loop Join(BNL)

Block Nested-Loop 和 Batched Key Access

MySQL Join算法与调优白皮书

Mysql join 算法原理

MySQL Join算法与调优白皮书