为什么不让用join?《死磕MySQL系列 十六》
大家好,我是咔咔 不期速成,日拱一卒
在平时开发工作中join的使用频率是非常高的,很多SQL优化博文也让把子查询改为join从而提升性能,但部分公司的DBA又不让用,那么使用join到底有什么问题呢?

一、什么是Nested-Loop Join
在MySQL中,使用Nested-Loop Join的算法进行优化join的使用,此算法翻译过来为嵌套循环连接
,并且使用了三种算法来实现。
Index Nested-Loop Join :简称NLJ Block Nested-Loop Join :简称BNLJ Simple Nested-Loop Join :简称 BNL
这几种算法大致意思为索引嵌套循环连接、缓存块嵌套循环连接、粗暴嵌套循环连接,你现在看的顺序就是MySQL选择join算法的优先级。
从名字上给人感觉Simple Nested-Loop Join
算法是非常简单同样也是最快的,但实际情况是MySQL并没有使用这种算法而是优化成使用Block Nested-Loop Join
,带着各种疑问一起来探索其中的奥秘。
都看到这里了,是不是对嵌套循环连接
的意思不太明白?其实是非常简单的,一个简单的案例你就能明白什么是嵌套循环连接。
假设现在有一张文章表article
,一张文章评论表article_detail
,需求是查询文章的id查询出所有的评论现在的首页,那么SQL就会是以下的样子
select * from article a left join article_detail b on a.id = b.article_id
若使用代码来描述这段SQL的实现原理大致如下,这段代码使用切片和双层循环实现冒泡排序,这段代码就能非常代表SQL中join的实现原理,第一层for即为驱动表,第二层for则为被驱动表。
func bubble_sort(arr []int) {
a := 0
for j := 0; j < len(arr)-1; j++ {
for i := 0; i < len(arr)-1-j; i++ {
if arr[i] > arr[i+1] {
a = arr[i]
arr[i] = arr[i+1]
arr[i+1] = a
}
}
}
}
好了,现在你知道了什么是Nested-Loop Join,也知道了实现Nested-Loop Join的三种算法,接下来咱们就围绕这三种算法来进行讨论,为什么不让用join。
二、Index Nested-Loop Join
为了防止优化器对SQL进行粗暴优化,接下来会使用STRAIGHT_JOIN
来进行查询操作。
为什么会需要STRAIGHT_JOIN
,在开发过程中有没有遇到明明是驱动表的却莫名其妙的成为了被驱动表,在MySQL中驱动表的概念是当指定了连接条件时,满足条件并记录行数少的表为驱动表。当没有指定查询条件时,则扫描行数少的为驱动表,优化器总是以小表驱动大表的方式来决定执行顺序的。
索引嵌套循环连接是基于索引进行连接的算法,索引是基于被驱动表的,通过驱动表查询条件直接与被驱动表索引进行匹配,防止跟被驱动表的每条记录进行比较,利用索引的查询减少了对被驱动表的匹配次数,从而提升join的性能。
使用前提
使用索引嵌套查询的前提是驱动表与被驱动表关联字段上有设置索引。
接下来使用一个案例来详细解析索引嵌套查询的具体执行流程,以下SQL是所有的表和数据,直接复制就可以用
CREATE TABLE `article` (`id` INT (11) NOT NULL AUTO_INCREMENT COMMENT 'ID',`author_id` INT (11) NOT NULL,PRIMARY KEY (`id`)) ENGINE=INNODB CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='文章表';
CREATE PROCEDURE idata () BEGIN DECLARE i INT; SET i=1; WHILE (i<=1000) DO INSERT INTO article VALUES (i,i); SET i=i+1; END WHILE; END;
call idata();
CREATE TABLE `article_comment` (`id` INT (11) NOT NULL AUTO_INCREMENT COMMENT 'ID',`article_id` INT (11) NOT NULL COMMENT '文章ID',`user_id` INT (11) NOT NULL COMMENT '用户ID',PRIMARY KEY (`id`),INDEX `idx_article_id` (`article_id`)) ENGINE=INNODB CHARSET=utf8mb4 COLLATE utf8mb4_german2_ci COMMENT='用户评论表';
DROP PROCEDURE idata;
CREATE PROCEDURE idata () BEGIN DECLARE i INT;
SET i=1; WHILE (i<=1000)
DO
INSERT INTO article_comment VALUES (i,i,i);
SET i=i+1; END WHILE; END;
CALL idata ();
可以看到,此时article表和article_comment,数据都是1000行
需求是查看文章的所有评论信息,执行SQL如下
SELECT*FROM article STRAIGHT_JOIN article_comment ON article.id=article_comment.article_id;
现在,我们来看一下这条语句的explain结果。

可以看到,在这条语句中,被驱动表article_comment的字段article_id使用了索引,因此这个语句的执行流程是这样的
从article表读取一行数据R 从R中去除id字段到表article_comment去查找 取出article_comment中满足条件的行,跟R组成一行 重复前三个步骤,直到表article满足条件的数据扫描结束
在这个流程中我们简单的梳理一下扫描行数
对article表需要做全表扫描,扫描行数为1000 没行R数据,根据article表的id去表article_comment查找,走的是树搜索,因此每次的搜索的结果都是一一对应的,也就是说每次只会扫描到一行数据,共需要扫描1000 所以,这个执行流程,总扫描行数为2000行
若在代码中如何实现
全表扫描article数据,这里是1000行 循环这1000行数据 使用article的id作为条件,在循环中进行查询
执行过程扫描行数也是2000行,先不涉及这样写性能如何,光与MySQL进交互就进行了1001次。
结论
显然这么做还不如直接使用join好
三、Simple Nested-Loop Join
简单嵌套循环连接查询是表连接使用不上索引,然后就粗暴的使用嵌套循环,article、article_comment表都有1000行数据,那么扫描数据的行数就是1000*1000=1千万,这种查询效率可想而知是怎么样的。
执行SQL如下
SELECT * FROM article STRAIGHT_JOIN article_comment ON article.author_id=author_id.user_id;
在这个流程里:
对驱动表article做了全表扫描,这个过程需要扫描1000行 从驱动表每读取一行数据都需要在article_comment表中进行全表扫描,没有使用索引就需要全表扫描 因此,每次都需要全表扫描被驱动表的数据
这还是两个非常小的表,在生产环境的表动辄就是上千万,如果使用这种算法估计MySQL就没有现在的盛况
当然了,MySQL也没有使用这种算法,而是用了分块嵌套查询的算法,这种思想在MySQL中很多地方都在使用
扩展
例如,索引是存储在磁盘中的,每次使用索引进行检索数据时会把数据从磁盘读入内存中,读取的方式也是分块读取,并不是一次读取完。
假设现在操作系统需在磁盘中读取1kb的数据,实际上会操作系统读取到4kb的数据,在操作系统中一页的数据是4kb,在innodb存储引擎中默认一页的数据是16kb。
为什么MySQL会采用分块来读取数据,是因为数据的局部性原理,数据和程序都有聚集成群的倾向,在访问到一行数据后,在之后有极大的可能性会再次访问这条数据和这条数据相邻的数据。
四、Block Nested-Loop Join
使用简单嵌套查询的方式经过上文的分析肯定是不可取的,而是选择了分块的思想进行处理。
这时,执行流程是这样的
从驱动表article中读取数据存放在join_buffer中,由于是使用的没有条件的select ,因此会把article全表数据放入内存 拿着join_buffer中的数据跟article_comment中的数据进行逐行对比
对应的,这条SQL的explain结果如下所示

为了复现Block Nested Loop
,咔咔装了三个版本的MySQL,分别为MySQL8,MySQL5.5,MySQL5.7在后两个版本中都使用的是Block Nested Loop
,但在MySQL8中却发生了变化。

对于hash join 下期会聊到,在这个查询过程中,对表article、article_comment都做了一次全表扫描,因此扫描行数是2000。
把article中的数据读取到join_buffer中是以无序数组的方式存储的,对于article_comment表中的每一行,都需要做1000次判断,那么就需要判断的次数就是1000*1000=1000万次。
这时你发现使用分块嵌套循环跟简单嵌套查询扫描行数是一样的,但Block Nested Loop
算法应用了join_buffer的这么一个内存空间,因此速度上肯定会比Simple
快很多。
五、总结
本期我们用三个问题来总结全文,以帮助你更好的理解。
第一个问题:能不能使用join?
通过三个演示案例,现在你应该知道当关联条件的列是被驱动表的索引时,是完全没有问题的,也就是说当使用索引嵌套查询时,是可以使用join的。
但当使用的是分块嵌套查询,这种方式扫描行数为两张表行数的乘,扫描行数会非常的大,会占用大量的系统资源,所以这种算法的join是非常不建议使用的。
因此当使用join时,最大可能的让关联查询的列为被驱动表的索引列,若不能达到这个条件则可以考虑表结构设计是否合理
第二个问题:如果使用join,选择大表还是小表作为驱动表?
好的习惯都是慢慢养成的,因此你要记住无论在什么情况下都用小表驱动大表,先记住这个结论。
如果是Nested-Loop Join
算法,应该选择小表作为驱动表。
如果是Block Nested-Loop Join
,当join_buffer足够大的时候,使用大表还是小表作为驱动表都是一样的,但是当join_buffer没有手动设置更大的值时,还是应该选择小表作为驱动表。
这里还需要知道一点join_buffer的默认值为在MySQL8.0为256kb。
第三个问题:什么样的表是小表?
这里的小表不是数据量非常小的表,这点一定不能搞错,在所有的SQL查询中绝大多数情况是有条件进行筛选的。
看是否为小表是根据同一条件下两张表那个检索的数据量小,那张表就是小表。
推荐阅读
打开order by的大门,一探究竟《死磕MySQL系列 十二》
闯祸了,生成环境执行了DDL操作《死磕MySQL系列 十四》
“
坚持学习、坚持写作、坚持分享是咔咔从业以来所秉持的信念。愿文章在偌大的互联网上能给你带来一点帮助,我是咔咔,下期见。
”
为什么不让用join?《死磕MySQL系列 十六》的更多相关文章
- 为什么不建议给MySQL设置Null值?《死磕MySQL系列 十八》
大家好,我是咔咔 不期速成,日拱一卒 之前ElasticSearch系列文章中提到了如何处理空值,若为Null则会直接报错,因为在ElasticSearch中当字段值为null时.空数组.null值数 ...
- 打开order by的大门,一探究竟《死磕MySQL系列 十二》
在日常开发工作中,你一定会经常遇到要根据指定字段进行排序的需求. 这时,你的SQL语句类似这样. select id,phone,code from evt_sms where phone like ...
- 闯祸了,生成环境执行了DDL操作《死磕MySQL系列 十四》
由于业务随着时间不停的改变,起初的表结构设计已经满足不了如今的需求,这时你是不是想那就加字段呗!加字段也是个艺术活,接下来由本文的主人咔咔给你吹. 试想一下这个场景 事务A在执行一个非常大的查询 事务 ...
- 聊聊MySQL的加锁规则《死磕MySQL系列 十五》
大家好,我是咔咔 不期速成,日拱一卒 本期来聊聊MySQL的加锁规则,知道这些规则后可以判断SQL语句的加锁范围,同时也可以写出更好的SQL语句,防止幻读问题的产生,在能力范围内最大程度的提升MySQ ...
- MySQL统计总数就用count(*),别花里胡哨的《死磕MySQL系列 十》
有一个问题是这样的统计数据总数用count(*).count(主键ID).count(字段).count(1)那个效率高. 先说结论,不用那么花里胡哨遇到统计总数全部使用count(*). 但是有很多 ...
- 为什么MySQL字符串不加引号索引失效?《死磕MySQL系列 十一》
群里一个小伙伴在问为什么MySQL字符串不加单引号会导致索引失效,这个问题估计很多人都知道答案.没错,是因为MySQL内部进行了隐式转换. 本期文章就聊聊什么是隐式转换,为什么会发生隐式转换. 系列文 ...
- 重重封锁,让你一条数据都拿不到《死磕MySQL系列 十三》
在开发中有遇到很简单的SQL却执行的非常慢,甚至只查询一行数据. 咔咔遇到的只有两种情况,一种是MySQL服务器CPU占用率很高,所有的SQL都执行的很慢直到超时,程序也直接502,另一种情况是行锁造 ...
- 一生挚友redo log、binlog《死磕MySQL系列 二》
系列文章 原来一条select语句在MySQL是这样执行的<死磕MySQL系列 一> 一生挚友redo log.binlog<死磕MySQL系列 二> 前言 咔咔闲谈 上期根据 ...
- MySQL强人“锁”难《死磕MySQL系列 三》
系列文章 一.原来一条select语句在MySQL是这样执行的<死磕MySQL系列 一> 二.一生挚友redo log.binlog<死磕MySQL系列 二> 前言 最近数据库 ...
随机推荐
- js源码-自定义数组的pop和shift方法
本文将自定义_pop和_shift来模拟数组的pop和shift方法 _pop: /* *js中数组的pop方法:删除数组的最后一个元素,把数组的长度减1,并且返回删除的这个元素:如果数组为空,则po ...
- SP19149 INS14H - Virus Revisited
可以发现,如果一个整体一起考虑是不能找到一个合适的状态来描述这个情形的. 因此可以考虑寻找整体的反面,也就是将每个维度分开考虑. 不难发现每个维度本质上是一样的,因此不需要考虑不同维度之间的区别. 那 ...
- python编写购物车新写法
用另一种方式完成购物车的功能实现 #!/usr/bin/python zijin = input("请输入资金:") if zijin.isdigit(): zijin = int ...
- SimpleDateFormat简介及替代方案
简介 SimpleDateFormat是一个时间格式化工具,可以将字符串格式化时间Date类型,也可以将Date类型格式化为字符串String类型,但其线程不安全. 常用方法 public final ...
- Linux-一次执行多个命令 ; && ||
一次执行多个命令,多个命令之间用:号隔开 cmd1;cmd2:cmd3 这样前后执行的时候没有依赖性,如果有下列要求呢 1. cmd1执行失败那就不要执行后面的命令 2. cmd1失败了才去指令后面的 ...
- 按照递推的思想求解next[]数组
按照递推的思想求解next[]数组 根据定义next[0]=-1,假设next[j]=k, 即P[0...k-1]==P[j-k,j-1] 若P[j]P[k],则有P[0..k]P[j-k,j],很显 ...
- Solution -「LOCAL」Burning Flowers
灼之花好评,条条生日快乐(假装现在 8.15)! \(\mathcal{Description}\) 给定一棵以 \(1\) 为根的树,第 \(i\) 个结点有颜色 \(c_i\) 和光亮值 ...
- 注意!你的 Navicat 可能被下毒了...
大家早上好,我是程序猿DD! 刚刚看到一份来自微步在线发布的威胁情报通报,其中提到了被我们广泛应用的数据库管理工具Navicat Premium被投毒消息!如果你有用过相关版本的话,可能当前正处于数据 ...
- Java常用工具类(自用)
统一响应格式 Response类 @JsonInclude(JsonInclude.Include.NON_NULL) public class Response<T> implement ...
- 【一天一个小知识10/20】Unity通过www获取json文本信息。
前提:领导要我在unity获取局域网服务器的文本信息.给了一个json的网络文本让我测试.我对于json以及服务器比较陌生.就直接去网上找相关的资料. 以下是自己测试的代码,没问题. 测试的网络jso ...