一般来说,使用join语句,会用到两种算法,分别是Index Nested-Loop Join(NLJ) 和 Block Nested-Loop Join(BNL)。
本文章介绍对这两个算法的优化。
建表:
create table t1(id int primary key, a int, b int, index(a));
create table t2 like t1;
drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t1 values(i, 1001-i, i);
set i=i+1;
end while;
set i=1;
while(i<=1000000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();如果插入得慢,可以尝试修改一下参数。
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; -- 改成 0或者2 提升性能
SHOW VARIABLES LIKE 'sync_binlog'; -- 改成0提升性能参考: https://blog.csdn.net/qq_36182135/article/details/84854619
Multi-Range Read 优化优化(MRR)是MySQL5.6开始引入的,这个优化的主要目的是尽量使用顺序读盘。
select * from t1 where a>=1 and a<=100;回表过程:

随着a的值顺序递增,id的值就变成随机的(这里是递降),出现随机访问的话,性能相对较差,可以调整查询顺序,起到加速作用。
因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
就是 MRR 优化的设计思路。
流程:
read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 2 和 3,然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。
MySQL5.6中,如果需要使用MRR,需要设置:
set optimizer_switch="mrr_cost_based=off"mrr_cost_based 设置为 off,就是固定使用 MRR,低于 MySQL5.6 版本设置会报错。
MMR优化后的执行流程:

于我们在 read_rnd_buffer 中按照 id 做了排序,所以最后得到的结果集也是按照主键 id 递增顺序的,也就是与第一个图片的 结果集中行的顺序相反。
MRR 能够提升性能的核心在于,这条查询语句在索引 a 上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键 id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势。
MySQL5.6版本之后,在MRR性能原理下,继续引入了Batched Key Access(BKA) 算法,对 NLJ 算法继续优化。
NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了。
BKA思路:从表t1中多拿些行出来(部分数据),先放到应该临时内存(join_buffer)。
join_buffer 在 BNL 算法里的作用,是暂存驱动表的数据。但是在 NLJ 算法里并没有用join_buffer 。那么,我们刚好就可以复用 join_buffer 到 BKA 算法中。
NLJ 算法加入join_buffer 优化后的 BKA 算法的流程:

注:如果 join buffer 放不下 P1~P100 的所有数据,就会把这 100 行数据分成多段执行。
启用BKA算法:
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';其中,前两个参数的作用是要启用 MRR。这么做的原因是,BKA 算法的优化要依赖于 MRR。
使用 Block Nested-Loop Join(BNL) 算法时,可能会对被驱动表做多次扫描。如果这个被驱动表是一个大的冷数据表,除了会导致 IO 压力大以外,还会影响、buffer_pool的正常运作。
优化后的LRU算法:
第一次从磁盘读入内存的数据页,会先放在 old 区域。如果 1 秒之后这个数据页不再被访问了,就不会被移动到 LRU 链表头部,这样对 Buffer Pool 的命中率影响就不大。
但是,如果一个使用 BNL 算法的 join 语句,多次扫描一个冷表,而且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到 LRU 链表头部。
这种情况对应的,是冷表的数据量小于整个 Buffer Pool 的 3/8,能够完全放入 old 区域的情况。 如果这个冷表很大,就会出现另外一种情况:业务正常访问的数据页,没有机会进入 young 区域。
由于优化机制的存在,一个正常访问的数据页,要进入 young 区域,需要隔 1 秒后再次被访问到。但是,由于我们的 join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页,很可能在 1 秒之内就被淘汰了。这样,就会导致这个 MySQL 实例的 Buffer Pool 在这段时间内,young 区域的数据页没有被合理地淘汰。
大表 join 操作虽然对 IO 有影响,但是在语句执行结束后,对 IO 的影响也就结束了。但是,对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。
为了减少这种影响,可以考虑增大 join_buffer_size 的值,减少对被驱动表的扫描次数。
也就是说,BNL 算法对系统的影响主要包括三个方面:
解决: explain 是否使用了BNL算法,如果是则做优化,给被驱动表的join字段加上索引,把BNL算法转成BKA算法。
些情况下,我们可以直接在被驱动表上建索引,这时就可以直接转成 BKA 算法了。
但是也有不适合在被驱动表上建索引的情况,如:
select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;只查了2000条数据,而且也不是高频查询,在百万数据表t2的b字段上建索引就浪费了。
BNL算法执行流程:
判断join 的 on 是否满足的时候,都需要遍历join_buffer中的所有行,因此判断的次数为 1000*100w 。
explain结果:

以上可以考虑临时表的思路:
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);过程消耗:
BNL算法优化: BNL算法,如果读取的是冷表,而且量比较大,循环读取,第一次数据会进入old区域,如果一秒之后没有访问,不会移到LRU头部,大表join对io影响查询完就结束了,但是buffer pool需要大量的查询把冷数据冲掉。BNL算法有3个问题,1 多次扫描被驱动表,占用磁盘io, 2 判断join会耗费大量的cpu资源, 3 会热数据淘汰,影响buffer pool的命中率