互联网上一直流传着各大公司的 MySQL 军规,其中关于 join 的描述,有些公司不推荐使用 join,而有些公司则规定有条件的使用 join, 它们都是教条式的规定,也没有详细说其中的原因,这就很容出现只知道这么用,但是不知道为什么的情况

那到底能不能使用 join, 什么情况下适合用join,什么情况下不适合用 join, join 有哪些使用原则呢?

本文将详细讲述 join 的执行流程、分析 join 的复杂度,并解答上面的几个常见问题,让读者能详细了解 join 的原理,做到知其然,知其所以然

测试数据准备

为了更好的分析 join 的工作原理,需要准备一些测试数据,我们分别创建 tatb 两个表,定义 insert_data 函数,然后往 tatb 表添加测试数据,具体如下所示:

  • 创建测试表
Create Table: CREATE TABLE `ta` (
`id` int(11) NOT NULL,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 Create Table: CREATE TABLE `tb` (
`id` int(11) NOT NULL,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 定义插入数据函数

定义一个插入数据的函数,用于往 tb 表中插入数据

delimieter ;;

create procedure insert_data()

begin

    declare i int;

    set i=1;

    while( i <= 1000 ) do

        insert into tb values(i,i,i);

        set i=i+1;

    end while;

end;;

delimiter ;

  • 添加测试数据

调用 insert_data 函数 往tb 表 插入1000条数据,tb表前100条数据插入到 ta 表中

mysql> call insert_data();

mysql> insert into ta (select * from tb where tb.a <= 100);

简述

MySQL中join主要用到 Simple nested-loop Block nested-loopIndex nested-loop 三种算法,下面针对这几种算法的流程逐一进行分析,在这之前,有几点需要说明下

  1. 本文所有测试都是在 mysql5.7 版本上进行的

  2. 由于mysql会对join的连接方式做优化,为了便于分析 join 的执行过程,文中的 sql 语句 统一使用我们指定的连接方式执行查询,关键字是 straight_join

Simple nested-loop 算法

在 MySQL中执行 select * from ta straight_join tb on ta.a = tb.b; ,语句的 explain 的结果如下:

在这个语句中,ta 是驱动表,tb 是被驱动表,由于表 tbb 字段没有索引,所以针对 tb 表需要走全表扫描

语句的执行流程如下:

  1. ta 表读取一条数据,记为Da
  2. Da 中取出字段 atb 表查询
  3. 取出 tb 表满足条件的数据,跟 Da数据组合成一行,作为结果集返回
  4. 接着读取 ta 表剩下的数据,重复执行步骤1 到 步骤3,直到读取完 ta 表记录

上面的流程是循环读取 ta 表记录,把每一行记录中的 a字段值, 去 tb 表中查询满足条件的记录,这个过程就类似下面代码中的嵌套for循环

for each row in ta matching range
{
for each row in tb matching
{
if row satisfies join conditions, send to client
}
}
  • 性能分析

每读取一条 ta 表记录,就需要扫描一次 tb 表的记录并与记录中 b 字段的值进行比较

所以总共扫描了 100 * 1000 = 10 万 行记录,进行了 10万次 比较操作

可以看出,总的扫描行数和总比较次数跟 tatb 表总记录数有关,记录数越大,总扫描行数和比较次数越大

假如 ta 表和 tb 表总记录数 各增加 10 倍,那么,总的扫描行数和比较次数会增加 100 倍,这个效率就非常低下了

基于以上原因,MySQL 没有选择使用 Simple nested-loop 算法了,而是使用了 Block nested-loop 算法

Block nested-loop算法

Block nested-loop 算法对 Simple nested-loop 算法进行了优化,它引入了 join buffer

join buffer 主要用于优化不带索引条件的 join 查询,它会缓存连接 过程中的用到的字段,对于减少扫描次数和比较次数很有帮助

join_buffer_size 参数可以控制 join_buffer 的大小,MySQL 默认的

join_buffer_size 是 256K,可以通过 select @@join_buffer_size

查询目前设置的大小

现以执行 select * from ta straight_join tb on ta.a = tb.b; 语句为例来说说明 Block nested-loop的执行流程

  1. ta 表所有记录读入 join buffer 中,join buffer 只缓存连接过程中必须的数据,这里使用了 select * ,所以,ta 表的记录的所有字段都会放入 join buffer 中

  2. 扫描 tb 表,读取每一行记录,跟 join buffer 中的数据对比,满足条件的数据,将会作为结果集返回

  • 性能分析

整个过程中,读取 ta 表所有记录,读取 tb 表所有记录,相当于 扫描了一遍 ta 表和 tb表,所以扫描总行数是 100 + 1000 = 1100 行

逐行遍历 tb 表,与 join buffer 中的数据比较,由于 join buffer 是无序的,所以,对于 tb 表每一条记录,都需要在 join buffer 中比较 100 次,因此总的比较次数是 1000 * 100 = 10万次

Simple nested-loop 比起来,由于有 join buffer 的帮助,Block nested loop 总扫描行数减少了很多,总共扫描了 100 + 1000 = 1100 行

虽然两者总比较次数都是 10 万次,但是,Block nested loop 的比较是在内存中的 join buffer 中进行的,所以,速度上应该会快很多,性能相对也好一些

如果驱动表 ta 数据行数是 M ,被驱动表 tb 数据行数是 N ,总的扫描行数为 M + N , 总的比较次数为 M * N , 所以整个执行过程 近似的复杂度是:M + N + M * N

上述复杂度公式中,M 和 N 调换的话,复杂度不变,所以,这时候选择小表还是大表作为驱动表都一样

join buffer 大小不够用的问题

上面 Block nested loop 执行流程中,第一步是把 整个ta表数据全部读入 join buffer 中,这里由于 ta 表数据少,join buffer 可以放得下全部的数据

如果 join buffer 的大小不足以缓存 ta表所有数据,该怎么办呢?这时候的执行流程又是怎样的呢 ?

答案是:join buffer 分段缓存 ta 表数据,处理完之后,清空缓存,然后再缓存 ta 表剩下的数据

现假设 join buffer 只能放得下 60 行 ta 表记录,执行流程就变成了:

  1. 遍历 ta 表,顺序读取 60 条记录放入 join buffer 中,此时,join buffer 满了

  2. 遍历 tb 表,取出得每一行,跟 join buffer 中得数据比较,组合满足条件得数据,作为结果集返回

  3. 清空 join buffer

  4. 接着上次的位置继续顺序扫描 ta 表,把剩下得 40行数据读入 join buffer 中,紧接着执行第 2 步 到 第 4 步,直到 读取完 ta 表记录

  • 性能分析

由于 ta 表是分两次读入 join buffer 的,所以需要扫描两次 tb 表,所以总扫描行数是 100 + 1000 * 2 = 2100 行

总的比较次数依然保持不变,是:(60 + 40) * 1000 = 10万次

从上面的结果可以看出,join buffer 分段缓存 的性能要比 一次缓存全部驱动表必需数据的方式 要差一些,也就是说,join buffer 分的段数越多,性能相对越差,在驱动表数量行数不变的情况下,分段数的取决于 join_buffer_size 参数的大小,这个参数越大,分段数或越小,反之越大, 所以有些地方建议通过调大 join_buffer_size 参数来提升 join 查询速度的方法,原因也在于此

如果驱动表 ta 数据行数是 M ,被驱动表 tb 数据行数是 N, join buffer 分段数是 K ,则总扫描行数为 M + K * N , 总的比较次数为 M * N , 所以整个执行过程 近似的复杂度是: M + K * N + M * N

显然 K 越小,复杂度越小,性能就越好,K 的最小值是 1 ,也就是 驱动表中所必需的字段能全部缓存到 join buffer 中

而 K 是与 驱动表数据行数 M 和 join_buffer_size 参数相关的,后者通常不会经常变化,所以 M 越小, K 就越小,K 越小,复杂度越小,性能就越好,K 的最小值是 1 ,也就是 驱动表中所必需的字段能全部缓存到 join buffer 中

因此,选择小表作为驱动表,查询性能更好

Index nested-loop算法

分析完上面两种算法,接着来看下 Index nested-loop 算法的执行流程

在 MySQL 中执行 select * from ta straight_join tb on ta.a = tb.a;, 语句的 explain 结果如下:

上述结果中,被驱动表 tb 的字段 a 上有索引,所以,连接的过程中能用上索引,它的执行流程是:

  1. 读取一行 ta 表记录, 记为 Da

  2. Da 中取出 a 字段去 tb 表查询

  3. 读取 tb 表中满足条件的记录,和 Da 组合,作为结果集返回

  4. 重复执行 步骤1 到 步骤 3,直到读取完 ta 表的记录

  • 性能分析

上面的流程是遍历 ta 表,然后从读出的每行记录中取出 a 的值 去 tb 表查询

由于 tb 表的 a 字段上有索引,所以查询 tb 表记录的时候,走的是 B+ 树的查询

我们准备的测试数据中 taa 字段和tba 字段是一一对应的,因此对于 ta 表的一行记录,在 tb 表中需要做两次 B+ 树查询,一次是普通索引树的查询,一次是回表查询

总的扫描行数是: 100 + 100 = 200 行( tb 表的两次B+树查询是由扫描一次表导致的,所以这里把tb表总扫描次数当作100次)

总的比较次数是: 100 次

如果驱动表 ta 数据行数是 M ,被驱动表 tb 数据行数是 N, 对于驱动表的一行数据,被驱动表需要先查询 a 索引的 B+ 树,再查询主键索引 B+ 树,所以被驱动表查询一行的复杂度是:2 * log2N

总扫描行数为 M + M * 2 * log2N , 总的比较次数为 M , 所以整个执行过程 近似的复杂度是: M + M * 2 * log2N + M = 2 * M * ( 1 + log2N )

近似复杂度公式中,变量是 M 和 N , 很显然,M 对结果影响更大,M 表示的是 驱动表 的数据行数,因此,选择 小表 作为驱动表能显著降低复杂度,提升查询速度

结果分析

上面分析了 Simple nested loop Block nested loopIndex nested loop 这三种算法的执行流程,并详细分析了每种算法的复杂度,根据分析的结果就可以回答本文开头的几个问题了

  • 能不能使用join
  1. 如果能用上被驱动表上的索引,即可以用上 Index nested loop 算法的话,是可以使用 join 的

  2. 如果使用 Block nested loop 算法的话,扫描行数和比较次数比较多,会占用大量的系统资源,特别是对于大表来说,查询速度和系统性能是无法接受的,所以,这种情况下,能不用join就不用join了

如果 explain 结果的 Extra 字段包含 ' Using join buffer (Block Nested Loop) ' 字符串的话,表示使用了 ' Block nested loop ' 算法

  • join 使用原则

在分析 Index nested loop 的复杂度之后,得出一个结论: 选择小表作为驱动表,查询性能更好

对于 Block nested loop 的复杂度分两种情况

  1. join buffer 没有分段, 此时不论选择小表还是大表作为驱动表,复杂度都一样

  2. join buffer 分段了,此时选择小表作为驱动表,复杂度更低,查询性能更好,相比join buffer 没有分段,实际的场景中,join buffer 分段的情况应该更多

综合来讲: 使用 join 应该选择小表 作为驱动表

  • 如何选择 "小" 表

上面 join 使用原则 中讲到选择小表作为驱动表,这里的 小表 并不是指表数据行数少,而是指参与join的表,按照查询条件分别得到结果集,结果集小的表作为驱动表

比如:有以下两个 SQL 语句

语句1:select * from ta straight_join tb on ta.b = tb.b where tb.id <= 20;
语句2:select * from tb straight_join ta on ta.b = tb.b where tb.id <= 20;

ta 表和 tb表么一行数据大小是一样的,语句1 使用 ta 作为驱动表,需要把 ta 表所有行数据(100行)放入 join buffer 中,但是 语句2 使用 tb 作为驱动表,只需要把 tb表前20行数据放入 join buffer, 也即 tb 表只有20行数据参与join, 这么一比较,tb 表查询的结果集更小,所以应该选择数据行数更多但是查询的结果集更小的 tb 表作为驱动表

小结

本文详细讨论了 Simple nested loop Block nested loopIndex nested loop 这三种算法,根据算法的执行流程定量的分析了各个算法的复杂度,再根据复杂度分析了 join 常见的一些问题,更多关于 join 的介绍请参考 MySQL 官网

到底能不能用 join的更多相关文章

  1. 关于T-SQL中exists或者not exists子查询的“伪优化”的做法

    问题起源 在使用t-sql中的exists(或者not exists)子查询的时候,不知道什么时候开始,发现一小部分人存在一种“伪优化”的一些做法,并且向不明真相的群众传递这一种写法“优越性”,实在看 ...

  2. 34 | 到底可不可以使用join?

    在实际生产中,关于 join 语句使用的问题,一般会集中在以下两类: 我们 DBA 不让使用 join,使用 join 有什么问题呢? 如果有两个大小不同的表做 join,应该用哪个表做驱动表呢? 今 ...

  3. MySQL分页优化中的“INNER JOIN方式优化分页算法”到底在什么情况下会生效?

    本文出处:http://www.cnblogs.com/wy123/p/7003157.html 最近无意间看到一个MySQL分页优化的测试案例,并没有非常具体地说明测试场景的情况下,给出了一种经典的 ...

  4. Java中Thread类的join方法到底是如何实现等待

    现在的场景是A线程执行:public void run(){ bThread.join(0);//把b线程加入到当前线程(a线程),等待b结束,当前a线程才会结束.}B线程执行public void ...

  5. 《Mysql - 到底可不可以使用 Join ?》

    一:Join 的问题? - 在实际生产中,使用 join 一般会集中在以下两类: - DBA 不让使用 Join ,使用 Join 会有什么问题呢? - 如果有两个大小不同的表做 join,应该用哪个 ...

  6. SQL Server-聚焦IN VS EXISTS VS JOIN性能分析(十九)

    前言 本节我们开始讲讲这一系列性能比较的终极篇IN VS EXISTS VS JOIN的性能分析,前面系列有人一直在说场景不够,这里我们结合查询索引列.非索引列.查询小表.查询大表来综合分析,简短的内 ...

  7. 关于join时显示no join predicate的那点事

    我们偶尔,非常偶尔的情况下会在一个查询计划中看到这样的警告: 大红叉,好吓人啊! 把鼠标放上去一看显示这样的信息 No join predicate 直译过来就是:没有连接谓词 在真实的生产环境下我们 ...

  8. 轻量级ORM框架——第二篇:Dapper中的一些复杂操作和inner join应该注意的坑

       上一篇博文中我们快速的介绍了dapper的一些基本CURD操作,也是我们manipulate db不可或缺的最小单元,这一篇我们介绍下相对复杂 一点的操作,源码分析暂时就不在这里介绍了. 一:t ...

  9. SQL语句到底是怎么执行的

    写在前面的话:有时不理解SQL语句各个部分执行顺序,导致理解上出现偏差,或者是书写SQL语句时随心所欲,所以有必要了解一下sql语句的执行顺序.可以有时间自己写一个简单的数据库,理解会更加深入.下面就 ...

随机推荐

  1. elasticsearch入门到放弃之elasticsearch-head

    elasticsearch-head可理解为跟DBeaver一样是一个数据可视化工具,但是这个工具并没有理想中那么好用坑也是很多,我已经在我的github上fork了一份修改后的版本:https:// ...

  2. 【转】shell中的$0 $n $# $* $@ $? $$ 变量 if case for while

    shell中的$0 $n $# $* $@ $? $$  shell 编程 | shift 命令用法笔记 $0当前脚本的文件名 $n传递给脚本或函数的参数.n 是一个数字,表示第几个参数.例如,第一个 ...

  3. Vue3.x全家桶+vite+TS-搭建Vue3.x项目

    目录 一.搭建基础项目 1.vite创建项目 3.运行项目 2.环境变量设置介绍 vite配置多环境打包 二.配置Router 1.安装路由 2.配置路由 3.引入 三.配置Vuex 1.安装vuex ...

  4. PTA 面向对象程序设计6-2 统计数字

    对于给定的一个字符串,统计其中数字字符出现的次数. 类和函数接口定义: 设计一个类Solution,其中包含一个成员函数count_digits,其功能是统计传入的string类型参数中数字字符的个数 ...

  5. umi request 请求资源库详解

    umi-request: 网络请求库,基于fetch封装,兼具fetch 和 axios 的所有特点,具有缓存,超时,字符编码处理,错误处理等常用功能. 1 支持url 参数自动序列化. 2 post ...

  6. 使用阿里云CDN后,php使用$_SERVER['HTTP_VIA']判断是否是移动端会出错

    使用阿里云CDN后,php使用$_SERVER['HTTP_VIA']判断是否是移动端会出错 if (isset ($_SERVER['HTTP_VIA'])) return stristr($_SE ...

  7. 『Python』matplotlib常用图表

    这里简要介绍几种统计图形的绘制方法,其他更多图形可以去matplotlib找examples魔改 1. 柱状图 柱状图主要是应用在定性数据的可视化场景中,或是离散数据类型的分布展示.例如,一个本科班级 ...

  8. sublime text 3 在Windows下配置sublimelinter-php的路径问题

    首先用package control安装sublimelinter和sublimelinter-php,然后依次点击菜单preference-package settings-sublimelinte ...

  9. 鸿蒙内核源码分析(Shell编辑篇) | 两个任务,三个阶段 | 百篇博客分析OpenHarmony源码 | v71.01

    子曰:"我非生而知之者,好古,敏以求之者也." <论语>:述而篇 百篇博客系列篇.本篇为: v71.xx 鸿蒙内核源码分析(Shell编辑篇) | 两个任务,三个阶段 ...

  10. pkusc2021游记

    @ 目录 前言 Day 0 Day 1 Day 2 Day 3 前言 到时候APIO的大概也会写在这篇里吧. Day 0 车,公交,飞机,公交,车 坐了半天的交通终于到了,整个人都坐的晕乎乎的,然后看 ...