文档章节

Mysql Join语句执行流程

12叔
 12叔
发布于 02/13 21:10
字数 2254
阅读 3.1K
收藏 6

今天我们来看一下join语句的执行流程

JOIN主要使用 Index Nested-Loop Join 和 Block Nested-Loop Join 算法实现

Index Nested-Loop Join

如果 join on 相关的字段存在索引就使用 Index Nested-Loop Join 算法来进行关联

如下sql语句的执行过程:

select * from t1 join t2 on (t1.a=t2.a);
  1. 对驱动表 t1 做了全表扫描,这个过程需要扫描 100 行;
  2. 而对于每一行 R,根据 a 字段去表 t2 查找,走的是树搜索过程。由于我们构造的数据 都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描 100 行;
  3. 所以,整个执行流程,总扫描行数是 200。

假设不使用 join,那我们就只能用单表查询。我们看看上面这条语句的需求,用单表查询 怎么实现。

  1. 执行select * from t1,查出表 t1 的所有数据,这里有 100 行;
  2. 循环遍历这 100 行数据: 从每一行 R 取出字段 a 的值 $R.a; 执行select * from t2 where a=$R.a; 把返回的结果和 R 构成结果集的一行。

可以看到,在这个查询过程,也是扫描了 200 行,但是总共执行了 101 条语句,比直接 join 多了 100 次交互。除此之外,客户端还要自己拼接 SQL 语句和结果

显然,这么做还不如直接 join 好。

怎么选择驱动表?

假设驱动表的行数是 N,执行过程就要扫描驱动表 N 行,然后对于每一行,到被驱动表上匹配一次

因此整个执行过程,近似复杂度是 N + N2log2M。显然,N 对扫描行数的影响更大,因此应该让小表来做驱动表

结论:

  1. 使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;
  2. 如果使用 join 语句的话,需要让小表做驱动表。

但是,你需要注意,这个结论的前提是“可以使用被驱动表的索引”。

Block Nested-Loop Join

如果关联语句中没有索引的话 可能需要扫描 N*M 行 当然Mysql对此有一个优化算法

算法的流程是这样的:

  1. 把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
  2. 扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条 件的,作为结果集的一部分返回。

Block Nested-Loop Join 算法的这 N*M 次判断是内存操作,速度上会快很多,性能也更好

接下来,我们来看一下,在这种情况下,应该选择哪个表做驱动表。

  1. 两个表都做一次全表扫描,所以总的扫描行数是 M+N;
  2. 内存中的判断次数是 M*N。

可以看到,调换这两个算式中的 M 和 N 没差别,因此这时候选择大表还是小表做驱动 表,执行耗时是一样的。

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 步

假设,驱动表的数据行数是 N,需要分 K 段才能完成算法流程,被驱动表的数据行数是 M。 注意,这里的 K 不是常数,N 越大 K 就会越大,因此把 K 表示为λ*N,显然λ的取值范围 是 (0,1)。 所以,在这个算法的执行过程中:

  1. 扫描行数是 N+λ* N*M;
  2. 内存判断 N*M 次。

显然,内存判断次数是不受选择哪个表作为驱动表影响的。而考虑到扫描行数,在 M和 N大小确定的情况下,N小一些,整个算式的结果会更小。

所以结论是,应该让小表当驱动表。

这里我需要说明下,什么叫作“小表”。

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤, 过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表

join语句怎么优化

Multi-Range Read 优化

在介绍 join 语句的优化方案之前,我需要先介绍一个知识点,即:Multi-Range Read 优化 (MRR)。这个优化的主要目的是尽量使用顺序读盘

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

这,就是 MRR 优化的设计思路。此时,语句的执行流程变成了这样:

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

另外需要说明的是,如果你想要稳定地使用 MRR 优化的话,需要设置

set optimizer_switch="mrr_cost_based=off"

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

Batched Key Access

这个 Batched Key Access (BKA),其实就是对 NLJ 算法的优化

NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了

那怎么才能一次性地多传些值给表 t2呢?方法就是,从表 t1 里一次性地多拿些行出来, 一起传给表 t2。 既然如此,我们就把表t1 的数据取出来一部分,先放到 join_buffer。 我们知道 join_buffer 在 BNL 算法里的作用,是暂存驱动表的数据。但是在 NLJ 算法里并没有用。那么,我们刚好就可以复用 join_buffer 到 BKA 算法中。

如果要使用 BKA 优化算法的话,你需要在执行 SQL 语句之前,先设置

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

BNL 算法的性能问题

大表 join 操作虽然对 IO 有影响,但是在语句执行结束后,对 IO 的影响也就结束了。但是,对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率

为了减少这种影响,你可以考虑增大 join_buffer_size 的值,减少对被驱动表的扫描次数。

也就是说,BNL 算法对系统的影响主要包括三个方面:

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

我们执行语句之前,需要通过理论分析和查看 explain 结果的方式,确认是否要使用 BNL 算法。如果确认优化器会使用 BNL 算法,就需要做优化。优化的常见做法是,给被驱动表 的 join 字段加上索引,把 BNL 算法转成 BKA 算法

hash join

如果 join_buffer 里面维护的不是一个无序数组,而是一个哈希表的话,那么就不是 10 亿次判 断,而是 100 万次 hash 查找。这样的话,整条语句的执行速度就快多了吧?

实际上,这个优化思路,我们可以自己实现在业务端。实现流程大致如下:

  1. select * from t1;取得表 t1 的全部 1000 行数据,在业务端存入一个 hash 结构
  2. select * from t2 where b>=1 and b<=2000; 获取表 t2 中满足条件的 2000 行 数据。
  3. 把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。

© 著作权归作者所有

12叔

12叔

粉丝 168
博文 43
码字总数 87275
作品 3
杭州
架构师
私信 提问
加载中

评论(0)

MySQL数据库中like语句及相关优化器 tips

背景 MySQL中在对某个字段做包含匹配时可以用like。 先看这个结构和结果 由于like用的是 ‘%xx%’, 不符合前缀匹配的规则,因此用不上索引title,只能作全表扫描。 问题 以上为官方回答。但是...

虫虫
2012/09/28
708
1
mysql_一条查询语句的执行流程

mysql的逻辑架构 一条查询语句的执行流程 连接器 查询缓存 但是大多数情况下建议不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。 分析器 如果没有命中查询缓存,就要开始真正执行语...

grace_233
2018/11/22
119
0
你知道select count(*)底层究竟干了啥么?

作者:贾春生 https://blog.didiyun.com/index.php/2019/01/08/mysql-count/ “SELECT COUNT( ) FROM T” 是个再常见不过的 SQL 需求了。在 MySQL 的使用规范中,我们一般使用事务引擎 Inno...

Java进阶架构师
2019/04/27
0
0
SQL总结

概要 SQL的增删改查操作的对象是数据库表中的记录 SQL语句的要素: 一是指明具体操作的关键字,insert、delete、update、select; 二是表名,缩小目标记录的范围; 三是条件表达式,对表中记...

bithup
2017/12/13
28
0
MySQL hints

我们可以对MySQL的对象(表、索引、触发器、自建函数、存储过程等)做注释(comment),这样做的目的是标识该对象的作用等以增强代码的可读性、方便其他同事快速读懂我们写的代码或某个数据库...

大王叫我来巡山Zd
2016/04/11
17
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周一乱弹 —— 提前接受社会的毒打教育

Osc乱弹歌单(2020)请戳(这里) 【今日歌曲】 @爱吃蛋挞的kk:分享Buddy Guy的单曲《I Need You Tonight》 《I Need You Tonight》- Buddy Guy 手机党少年们想听歌,请使劲儿戳(这里) 凌...

小小编辑
9分钟前
23
0
成都哪里可以开租赁费发票-成都新闻网

成都哪里可以开租赁费发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用了...

提供格
12分钟前
21
0
成都哪里可以开钢材发票-成都新闻网

成都哪里可以开钢材发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用了该...

多徐重
13分钟前
17
0
成都哪里可以开医疗器械发票-成都新闻网

成都哪里可以开医疗器械发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用...

识过人石
15分钟前
43
0
函数

if ( ) {return 10;}else {return 20;}//“单一出口”理念,虽然上面这么写可以,但是最好只有一个return//把需要返回的值赋值给一个变量,最后统一返回,这样将来修改方便 void ...

heronos
26分钟前
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部