mysql中join很慢?是你用的姿势不对吧 2

上一篇文章,我们说到,在被驱动表可以使用索引的情况下,join的执行效率还是很高的,为了减少扫描的行数和sql执行的时间复杂度,我们通常会让数据量小的表作为索引表,数据量相对大的表作为被驱动表。这种可以使用索引的join,我们被称为 Index Nested-Loop Join(NLJ)。

在上面文章最后,提出了:当join过程中,被驱动表无法使用索引时,join语句执行效率会变慢很多。这篇文章,我们就来探究一下,为什么被驱动表在join的过程中不使用索引的情况下,会很慢,以及它执行过程是怎么样的。

为了便于讨论问题,我们把上一篇文章中的数据表结构,以及初始化数据,重新准备一下:

create table 'table1' (
'id' int(11) NOT NULL,
'a' int(11) DEFAULT NULL,
'b' int(11) DEFAULT NULL,
PRIMARY KEY ('id'),
KEY 'a' ('a')
) engine = Innodb;


insert into table1 values(1,1,1)
insert into table1 values(2,2,2)
...
insert into table1 values(3,3,3) // 可以使用存储过程来实现批量数据的插入

create table table2 like table1;
insert into table2 (select * from table1 where id <= 100)

不使用索引的join

为了在join过程中不使用索引,我们把sql语句改成如下:

select * from table2 straight_join table1 on table1.b = table2.b;

使用的是straight_join,可以明确table2为驱动表,table1为被驱动表,也就是小表作为驱动表。

因为在table1中,字段b上没有索引,当遍历table2中每一行数据,取出字段b的值,到table1中进行匹配的是时候,需要全表扫描table1。这样算下来,扫描驱动表的行数为100行。扫描被驱动表的行数为 100*1000=10w行。总共扫描的行数为 100100行。

如果sql语句,改成如下方式:

select * from table1 straight_join table2 on table1.b = table2.b;

将table1作为驱动表,table2作为被驱动表,也就是大表作为驱动表。

在这种情况下,按照上面的执行过程。扫描驱动表行数为1000行。扫描被驱动行数为100*1000=10w行。总共扫描的行数为101000行。

在这种没有使用索引的情况,无论大表作为索引表还是小表作为索引表,执行性能相差无几,总的扫描次数,大概为驱动表行数*被驱动表行数。

这种join执行算法,也就是我们常说的"笛卡尔乘积"。在本例中,驱动表和被驱动表中数据行数比较小,看不出这种算法执行效率,如果将两个表中的数据量,都扩大100倍的话,table1中数据行数为10w,table2中数据行数为1w的话,那么两个表join过程,需要扫描行数大概为10亿行。我在mysql中简单测试了一下,join的耗时大概在55s左右。

对于这种规模的两个表,如果join过程中可以使用到索引的话:

当table2作为驱动表时,总的扫描行数大概为 1w+1w=2w行。

当table1作为驱动表时:总的扫描行数大概为 10w+1w=10w行。

不使用索引的情况下扫描行数为10亿,使用索引的情况下扫描行数为2万,这下可以看出,在不使用索引的情况下,join语句的确效率很低。这种没有任何优化,直接使用笛卡尔积实现的join,被称为 Simple Nested-Loop Join。

这种join实现方式中:

驱动表所做的操作:被逐行遍历,也就是全表扫描。

被驱动表所做的操作:取出驱动表中每行数据R中的b值,$R.b,对被驱动表,进行全表扫描来完成匹配,将匹配到数据和R进行组合,构成数据集一部分数据。被驱动表全表扫描的次数等于驱动表的行数,被驱动表总的扫描行数为 驱动表行数 * 被驱动行数。

mysql对不使用索引的join查询,在实现的过程中,并没有使用Simple Nested-Loop Join的方式。而是使用了一种空间换时间的方式进行了优化,同时也改变了驱动表和被驱动的在join执行过程中的行为。

Block Nested-Loop Join

不使用索引的join,经过mysql优化后:

select * from table2 straight_join table1 on table1.b = table2.b;

执行过程如下:

1.把驱动表table2中的数据读取线程内存join_buffer中,因为查询需要展示的字段是  * ,所以需要将table2整个表都要放到 join_buffer中。

2.遍历被驱动表table1,取出table1中每行数据R中字段b值,$R.b,跟join_buffer中的数据基于内存进行匹配,匹配成功的,作为结果集的一部分返回。

在Simple Nested-Loop Join中:查找满足条件的数据行的过程中,需要不断的对被驱动表进行全表扫描,对被驱动表全表扫描的次数等于驱动表数据行数。
而在Block Nested-Loop Join中,因为事先已经将驱动表中的数据放到内存中,查找满足条件的数据,只需要执行内存操作即可,大幅度的减少了对被驱动表全表扫描的次数。这也是一种利用空间换时间的一种体现。

通过查看如下sql的执行计划:

explain select * from table1 straight_join table2 on (table1.b = table2.b);

执行计划图如下:

可以看到 extra中的内容:using join buffer。

当驱动表比较小,可以全部放到join buffer中时:join过程中,驱动表和被驱动表,都被扫描一次,总的扫描行数为 1000+100 = 1100次。基于内存执行join操作的次数,为1000*100=10w
当驱动表数据量比较大时:join_buffer可能无法完全容纳,这个时候,会将驱动表中的数据分批次存放到join_buffer中。
此时Block Nested-Loop Join 的执行就变成了如下过程:

1.从驱动表中取出一批数据存放到 join_buffer中。

2.遍历被驱动表,取出每一行数据,跟join_buffer中的数据进行对比匹配,把满足join条件的数据,作为记过集的一部分返回。

3.清空join_buffer。

4.重复执行1,2,3步,直到驱动表的数据被全部放到join_buffer中。

总结

我们假设驱动表行数为M行,被驱动表行数为N行,那么在整个过程中:
执行内存比较的次数为:N*(b1+b2+...+bx)次(b表示:每批次放入到join_buffer中数据行数,x表示:放入到join_buffer总批次数)。而 b1+b2+...+bx=驱动表的总行数。所以执行内存比较次数仍然为:M*N。

扫描的行数为:M+x*N。(x表示:驱动表放入到join_buffer总批次数)

由此可见,当驱动表和被驱动表行数固定的时候,Block Nested-Loop Join执行的性能,主要受到join_buffer的大小影响:

当join_buffer比较小的时候,驱动表中的数据分批放到join_buffer中的次数就会比较多,对被驱动表全表扫描的次数就比较多,整体扫描的行数也就比较多。

当join_buffer比较大的时候,驱动表中的数据分批放到join_buffer中的次数就会比较少,对被驱动表全表扫描的次数就比较少,整体扫描的行数也就比较少。

反过来说,当join_buffer固定的时候,驱动表中的数据放到join_buffer中的批次,取决于驱动表中数据量。驱动表中数据量越多,分批放到join_buffer中的次数就会越多,反之越少。

所以当join过程中无法使用索引的时候,我们可以通过让小表作为驱动表,或者调大join_buffer的大小,来加速join的执行效率。

Block Nested-Loop Join是mysql对无法使用索引的join的一种优化手段,但是当两个大表进行join的时候,执行效率还是会很低。针对这种情况,你有什么好的方法吗?

这里笔者的优化思想:主要是将 Block Nested-Loop Join转换成 Index Nested-Loop Join。

1:可以在必要的字段上增加索引,使join的过程中可以使用到这个索引,让 Block Nested-Loop Join转换成 Index Nested-Loop Join。

2:如果增加索引的代价过大的话,那么可以建立临时表,将被驱动表中的数据迁移到临时表中,在临时表上建立需要的索引。

因为在 Block Nested-Loop Join中,当两个表的数据比较大的时候,无论如何优化,性能都不会很高,只要朝着 Index Nested-Loop Join的方向优化才是正道。


 

猜你喜欢

转载自blog.csdn.net/weixin_45701550/article/details/111877230