Mysql Join语句执行流程

Wesley13
• 阅读 915

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 + N_2_log2M。显然,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 条 件的,作为结果集的一部分返回。郑州市不孕不育医院:http://www.03913882333.com/

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 条件 的,作为结果集的一部分返回;郑州市不孕不育专科医院:http://www.zzchbb.com/

  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 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
sql注入
反引号是个比较特别的字符,下面记录下怎么利用0x00SQL注入反引号可利用在分隔符及注释作用,不过使用范围只于表名、数据库名、字段名、起别名这些场景,下面具体说下1)表名payload:select\from\users\whereuser\_id1limit0,1;!(https://o
Stella981 Stella981
3年前
Python3:sqlalchemy对mysql数据库操作,非sql语句
Python3:sqlalchemy对mysql数据库操作,非sql语句python3authorlizmdatetime2018020110:00:00coding:utf8'''
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Android蓝牙连接汽车OBD设备
//设备连接public class BluetoothConnect implements Runnable {    private static final UUID CONNECT_UUID  UUID.fromString("0000110100001000800000805F9B34FB");
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这