首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >35 | join语句优化

35 | join语句优化

作者头像
HaC
发布2020-12-30 17:51:42
发布2020-12-30 17:51:42
1K0
举报
文章被收录于专栏:HaC的技术专栏HaC的技术专栏

一般来说,使用join语句,会用到两种算法,分别是Index Nested-Loop Join(NLJ) 和 Block Nested-Loop Join(BNL)。

  • 在使用 NLJ 算法的时候,效果还是不错的,比通过应用层拆分成多个语句然后再拼接查询结果更方便,而且性能也不会差。
  • BNL 算法在大表 join 的时候性能就差多了,比较次数等于两个表参与 join 的行数的乘积,很消耗 CPU 资源。

本文章介绍对这两个算法的优化。

建表:

代码语言:javascript
复制
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();

如果插入得慢,可以尝试修改一下参数。

代码语言:javascript
复制
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 优化

Multi-Range Read 优化优化(MRR)是MySQL5.6开始引入的,这个优化的主要目的是尽量使用顺序读盘。

代码语言:javascript
复制
select * from t1 where a>=1 and a<=100;

回表过程:

随着a的值顺序递增,id的值就变成随机的(这里是递降),出现随机访问的话,性能相对较差,可以调整查询顺序,起到加速作用。

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

就是 MRR 优化的设计思路。

流程:

  1. 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 ;
  2. 将 read_rnd_buffer 中的 id 进行递增排序;
  3. 排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。

read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 2 和 3,然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。

MySQL5.6中,如果需要使用MRR,需要设置:

代码语言:javascript
复制
set optimizer_switch="mrr_cost_based=off"

mrr_cost_based 设置为 off,就是固定使用 MRR,低于 MySQL5.6 版本设置会报错。

MMR优化后的执行流程:

于我们在 read_rnd_buffer 中按照 id 做了排序,所以最后得到的结果集也是按照主键 id 递增顺序的,也就是与第一个图片的 结果集中行的顺序相反。

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

Batched Key Access

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算法:

代码语言:javascript
复制
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 算法对系统的影响主要包括三个方面:

  1. 可能会多次扫描被驱动表,占用磁盘 IO 资源;
  2. 判断 join 条件需要执行 M*N 次对比(M、N 分别是两张表的行数),如果是大表就会占用非常多的 CPU 资源;
  3. 可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率。

解决: explain 是否使用了BNL算法,如果是则做优化,给被驱动表的join字段加上索引,把BNL算法转成BKA算法。

BNL 转 BKA

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

但是也有不适合在被驱动表上建索引的情况,如:

代码语言:javascript
复制
select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;

只查了2000条数据,而且也不是高频查询,在百万数据表t2的b字段上建索引就浪费了。

BNL算法执行流程:

  1. 把表 t1 的所有字段取出来,存入 join_buffer 中。这个表只有 1000 行,join_buffer_size 默认值是 256k,可以完全存入。
  2. 扫描表 t2,取出每一行数据跟 join_buffer 中的数据进行对比,
  • 如果不满足 t1.b=t2.b,则跳过;
  • 如果满足 t1.b=t2.b, 再判断其他条件,也就是是否满足 t2.b 处于[1,2000]的条件,如果是,就作为结果集的一部分返回,否则跳过。

判断join 的 on 是否满足的时候,都需要遍历join_buffer中的所有行,因此判断的次数为 1000*100w 。

explain结果:

以上可以考虑临时表的思路:

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

SQL如下:

代码语言:javascript
复制
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);

过程消耗:

  1. 执行 insert 语句构造 temp_t 表并插入数据的过程中,对表 t2 做了全表扫描,这里扫描行数是 100 万。
  2. 之后的 join 语句,扫描表 t1,这里的扫描行数是 1000;join 比较过程中,做了 1000 次带索引的查询。相比于优化前的 join 语句需要做 10 亿次条件判断来说,这个优化效果还是很明显的。

小结

  1. BKA 优化是 MySQL 已经内置支持的,建议你默认使用;
  2. BNL 算法效率低,建议你都尽量转成 BKA 算法。优化的方向就是给被驱动表的关联字段加上索引;
  3. 基于临时表的改进方案,对于能够提前过滤出小数据的 join 语句来说,效果还是很好的;

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/03/06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Multi-Range Read 优化
  • Batched Key Access
  • BNL 转 BKA
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档