为什么不让用join?《死磕MySQL系列 十六》

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 为什么不让用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结果如下所示


image.png


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


image.png


对于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查询中绝大多数情况是有条件进行筛选的。


看是否为小表是根据同一条件下两张表那个检索的数据量小,那张表就是小表。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
SQL 关系型数据库 MySQL
MySQL 之 LEFT JOIN 避坑指南
MySQL 之 LEFT JOIN 避坑指南
207 1
|
6月前
|
关系型数据库 MySQL
mysql join 实践
mysql join 实践
38 0
|
3月前
|
存储 关系型数据库 MySQL
mysql中的left join、right join 、inner join的详细用法
【8月更文挑战第16天】在MySQL中,`INNER JOIN`、`LEFT JOIN`与`RIGHT JOIN`用于连接多表。`INNER JOIN`仅返回两表中匹配的行;`LEFT JOIN`保证左表所有行出现于结果中,右表无匹配时以NULL填充;`RIGHT JOIN`则相反,保证右表所有行出现于结果中。例如,查询学生及其成绩时,`INNER JOIN`仅显示有成绩的学生;`LEFT JOIN`显示所有学生及他们对应的成绩,无成绩者成绩列为空;`RIGHT JOIN`显示所有成绩及对应学生信息,无学生信息的成绩条目则为空。
|
3月前
|
SQL 关系型数据库 MySQL
Mysql中from多表跟join表的区别
Mysql中from多表跟join表的区别
216 0
|
4月前
|
SQL Java 数据库
MySQL设计规约问题之为什么应尽量避免使用子查询,而可以考虑将其优化为join操作
MySQL设计规约问题之为什么应尽量避免使用子查询,而可以考虑将其优化为join操作
|
5月前
|
SQL 关系型数据库 MySQL
蓝易云 - Mysql join加多条件与where的区别
总的来说,JOIN和WHERE都是SQL查询的重要部分,但它们用于处理不同的问题:JOIN用于连接表,而WHERE用于过滤结果。
32 2
|
4月前
|
SQL 关系型数据库 MySQL
学习mysql中使用inner join,left join 等
学习mysql中使用inner join,left join 等
|
5月前
|
算法 关系型数据库 MySQL
深入理解MySQL中的JOIN算法
深入理解MySQL中的JOIN算法
|
6月前
|
SQL 存储 关系型数据库
深入理解MySQL中的UPDATE JOIN语句
MySQL的UPDATE JOIN语句用于根据关联表的条件更新数据。示例中,历史记录表有用户账号字段,新增列用于存储用户名。通过UPDATE JOIN,一次性将账号转换为用户名。关键点包括准确的连接条件、谨慎使用WHERE子句以及在更新前进行测试。此操作提高了数据处理效率,但也需小心操作以防止数据错误。
206 4
深入理解MySQL中的UPDATE JOIN语句
|
6月前
|
存储 算法 关系型数据库
MySQL的JOIN到底是怎么玩的
在MySQL中,查询操作通常会涉及到联结不同表格,而JOIN命令则在这一过程中扮演了关键角色。在JOIN操作中,我们通常会使用三种不同的方式,分别是内连接、左连接以及右连接。