到底能不能用 join

互联网上一直流传着各大公司的 MySQL 军规,其中关于 join 的描述,有些公司不推荐使用 join,而有些公司则规定有条件的使用 join, 它们都是教条式的规定,也没有详细说其中的原因,这就很容出现只知道这么用,但是不知道为什么的情况
那到底能不能使用 join, 什么情况下适合用join,什么情况下不适合用 join, join 有哪些使用原则呢?
本文将详细讲述 join 的执行流程、分析 join 的复杂度,并解答上面的几个常见问题,让读者能详细了解 join 的原理,做到知其然,知其所以然
测试数据准备
为了更好的分析 join 的工作原理,需要准备一些测试数据,我们分别创建 ta 和 tb 两个表,定义 insert_data 函数,然后往 ta 、 tb 表添加测试数据,具体如下所示:
- 创建测试表
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-loop、Index nested-loop 三种算法,下面针对这几种算法的流程逐一进行分析,在这之前,有几点需要说明下
本文所有测试都是在 mysql5.7 版本上进行的
由于mysql会对join的连接方式做优化,为了便于分析 join 的执行过程,文中的 sql 语句 统一使用我们指定的连接方式执行查询,关键字是 straight_join
Simple nested-loop 算法
在 MySQL中执行 select * from ta straight_join tb on ta.a = tb.b; ,语句的 explain 的结果如下:

在这个语句中,ta 是驱动表,tb 是被驱动表,由于表 tb 上 b 字段没有索引,所以针对 tb 表需要走全表扫描
语句的执行流程如下:
- 从
ta表读取一条数据,记为Da - 从
Da中取出字段a去tb表查询 - 取出
tb表满足条件的数据,跟Da数据组合成一行,作为结果集返回 - 接着读取
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万次 比较操作
可以看出,总的扫描行数和总比较次数跟 ta 和 tb 表总记录数有关,记录数越大,总扫描行数和比较次数越大
假如 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的执行流程
把
ta表所有记录读入 join buffer 中,join buffer 只缓存连接过程中必须的数据,这里使用了select *,所以,ta表的记录的所有字段都会放入 join buffer 中扫描
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 表记录,执行流程就变成了:
遍历
ta表,顺序读取 60 条记录放入 join buffer 中,此时,join buffer 满了遍历
tb表,取出得每一行,跟 join buffer 中得数据比较,组合满足条件得数据,作为结果集返回清空 join buffer
接着上次的位置继续顺序扫描
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 上有索引,所以,连接的过程中能用上索引,它的执行流程是:
读取一行
ta表记录, 记为Da从
Da中取出a字段去tb表查询读取
tb表中满足条件的记录,和Da组合,作为结果集返回重复执行 步骤1 到 步骤 3,直到读取完
ta表的记录
- 性能分析
上面的流程是遍历 ta 表,然后从读出的每行记录中取出 a 的值 去 tb 表查询
由于 tb 表的 a 字段上有索引,所以查询 tb 表记录的时候,走的是 B+ 树的查询
我们准备的测试数据中 ta 表 a 字段和tb表 a 字段是一一对应的,因此对于 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 loop、Index nested loop 这三种算法的执行流程,并详细分析了每种算法的复杂度,根据分析的结果就可以回答本文开头的几个问题了
- 能不能使用join
如果能用上被驱动表上的索引,即可以用上
Index nested loop算法的话,是可以使用 join 的如果使用
Block nested loop算法的话,扫描行数和比较次数比较多,会占用大量的系统资源,特别是对于大表来说,查询速度和系统性能是无法接受的,所以,这种情况下,能不用join就不用join了
如果 explain 结果的 Extra 字段包含 ' Using join buffer (Block Nested Loop) ' 字符串的话,表示使用了 ' Block nested loop ' 算法
- join 使用原则
在分析 Index nested loop 的复杂度之后,得出一个结论: 选择小表作为驱动表,查询性能更好
对于 Block nested loop 的复杂度分两种情况
join buffer 没有分段, 此时不论选择小表还是大表作为驱动表,复杂度都一样
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 loop、Index nested loop 这三种算法,根据算法的执行流程定量的分析了各个算法的复杂度,再根据复杂度分析了 join 常见的一些问题,更多关于 join 的介绍请参考 MySQL 官网
到底能不能用 join的更多相关文章
- 关于T-SQL中exists或者not exists子查询的“伪优化”的做法
问题起源 在使用t-sql中的exists(或者not exists)子查询的时候,不知道什么时候开始,发现一小部分人存在一种“伪优化”的一些做法,并且向不明真相的群众传递这一种写法“优越性”,实在看 ...
- 34 | 到底可不可以使用join?
在实际生产中,关于 join 语句使用的问题,一般会集中在以下两类: 我们 DBA 不让使用 join,使用 join 有什么问题呢? 如果有两个大小不同的表做 join,应该用哪个表做驱动表呢? 今 ...
- MySQL分页优化中的“INNER JOIN方式优化分页算法”到底在什么情况下会生效?
本文出处:http://www.cnblogs.com/wy123/p/7003157.html 最近无意间看到一个MySQL分页优化的测试案例,并没有非常具体地说明测试场景的情况下,给出了一种经典的 ...
- Java中Thread类的join方法到底是如何实现等待
现在的场景是A线程执行:public void run(){ bThread.join(0);//把b线程加入到当前线程(a线程),等待b结束,当前a线程才会结束.}B线程执行public void ...
- 《Mysql - 到底可不可以使用 Join ?》
一:Join 的问题? - 在实际生产中,使用 join 一般会集中在以下两类: - DBA 不让使用 Join ,使用 Join 会有什么问题呢? - 如果有两个大小不同的表做 join,应该用哪个 ...
- SQL Server-聚焦IN VS EXISTS VS JOIN性能分析(十九)
前言 本节我们开始讲讲这一系列性能比较的终极篇IN VS EXISTS VS JOIN的性能分析,前面系列有人一直在说场景不够,这里我们结合查询索引列.非索引列.查询小表.查询大表来综合分析,简短的内 ...
- 关于join时显示no join predicate的那点事
我们偶尔,非常偶尔的情况下会在一个查询计划中看到这样的警告: 大红叉,好吓人啊! 把鼠标放上去一看显示这样的信息 No join predicate 直译过来就是:没有连接谓词 在真实的生产环境下我们 ...
- 轻量级ORM框架——第二篇:Dapper中的一些复杂操作和inner join应该注意的坑
上一篇博文中我们快速的介绍了dapper的一些基本CURD操作,也是我们manipulate db不可或缺的最小单元,这一篇我们介绍下相对复杂 一点的操作,源码分析暂时就不在这里介绍了. 一:t ...
- SQL语句到底是怎么执行的
写在前面的话:有时不理解SQL语句各个部分执行顺序,导致理解上出现偏差,或者是书写SQL语句时随心所欲,所以有必要了解一下sql语句的执行顺序.可以有时间自己写一个简单的数据库,理解会更加深入.下面就 ...
随机推荐
- HDU 6170 FFF at Valentine(强联通缩点+拓扑排序)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6165 题意:给你一个无环,无重边的有向图,问你任意两点,是否存在路径使得其中一点能到达另一点 解析:强 ...
- JAVA语言程序设计课程评价
紧张的又短暂的一个学期结束了,这个学期也许将成为我人生中一个重要的转折点,作为一名半路出家的选手,在初次了解Java语言时我感到非常的迷茫与不知所措.因为之前很多同学都是通过假期时间在家自学,刚转入新 ...
- Java跨平台原理(字节码文件,虚拟机)
介绍 C/C++语言都直接编译成针对特定平台机器码.如果要跨平台,需要使用相应的编译器重新编译. Java源程序(.java)要先编译成与平台无关的字节码文件(.class),然后字节码文件再解释成机 ...
- Java on Visual Studio Code的更新 – 2021年8月
Nick Senior Program Manager, Developer Division at Microsoft 大家好,欢迎来到 8 月版的 Visual Studio Code Java ...
- 学习PHP中的任意精度扩展函数
今天来学习的是关于数学方面的第一个扩展.对于数学操作来说,无非就是那些各种各样的数学运算,当然,整个程序软件的开发过程中,数学运算也是最基础最根本的东西之一.不管你是学得什么专业,到最后基本上都会要学 ...
- mysql将语句写入表中
使用create table语句即可 CREATE TABLE membertmp (select a.* from member as a where a.phone <> '' and ...
- TP5缩放图片加水印
// 给图片增加水印文字 试验缩放图片,放大图片,加水印,加文字功能 public function doCreateImage1($data,$path) { $basePath = ROOT_PA ...
- javascript 求最大前5个数; 对象 深拷贝 deep copy
* 用数组 function getTopN(a, n) { function _cloneArray(aa) { var n = aa.length, a = new Array(n); for ( ...
- 华为云计算IE面试笔记-Fusionsphere架构及组件介绍(服务器虚拟化解决方案)
eDSK 最上层则是eDSK是我们FusionSphere服务器虚拟化解决方案中的虚拟化北向统一API接口,其他的第三方系统或者是其他运营平台(FC.VMware等)可以通过eDSK轻松完成无缝对 ...
- 51nod1836-战忽局的手段【期望dp,矩阵乘法】
正题 题目连接:http://www.51nod.com/Challenge/Problem.html#problemId=1836 题目大意 \(n\)个点\(m\)次随机选择一个点标记(可以重复) ...