服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Mysql - 一文弄懂Join语句优化

一文弄懂Join语句优化

2023-11-29 01:00未知服务器之家 Mysql

这一篇文章就来介绍一下关联查询的优化,文章有点长,请耐心看完,有问题欢迎讨论指正。 1 关联查询的算法特性总结 要想弄懂关联查询的优化,就必须先知道关联查询相关的算法: Join算法 解释 Simple Nested-Loop Join算法 遍历驱

这一篇文章就来介绍一下关联查询的优化,文章有点长,请耐心看完,有问题欢迎讨论指正。

1 关联查询的算法特性总结

要想弄懂关联查询的优化,就必须先知道关联查询相关的算法:

Join算法

解释

Simple Nested-Loop Join算法

遍历驱动表中的每一行,每一行再到被驱动表中全表扫描,如果满足关联条件,则返回结果

Index Nested-Loop Join算法

遍历驱动表中的每一行,都通过索引找到被驱动表中关联的记录,如果满足关联条件,则返回结果

Block Nested-Loop Join算法

把驱动表的数据读入到 join_buffer 中,把被驱动表每一行取出来跟 join_buffer 中的数据做对比,如果满足 join 条件,则返回结果

Hash Join算法

将驱动表的数据加载到内存中构建哈希表,然后逐行读取被驱动表的数据,并通过哈希函数将连接条件的列的值映射为哈希值,查找匹配的哈希值,最后返回匹配的结果给客户端,跟Block Nested-Loop Join算法类似,但是不需要将被驱动表的数据块写入内存或磁盘,更少的IO以及更节省资源

Batched Key Access算法

将驱动表中相关列放入 join_buffer 中

批量将关联字段的值发送到 Multi-Range Read(MRR) 接口

MRR 通过接收到的值,根据其对应的主键 ID 进行排序,然后再进行数据的读取和操作

返回结果给客户端

2 Simple Nested-Loop Join算法

一文弄懂Join语句优化图片

循环驱动表中的每一行

再到被驱动表找到满足关联条件的记录

因为关联字段没索引,所以在被驱动表里的查询需要全表扫描

这种方法逻辑简单,但是效率很差

比如驱动表数据量是 m,被驱动表数据量是 n,则扫描行数为 m * n

当然,好在,MySQL也没有采用这种算法,即使关联字段没索引,也会采用Block Nested-Loop Join或者Hash Join,等下会细说。

3 Index Nested-Loop Join算法

刚才我们说的是关联字段没索引的情况,假如关联字段有索引,就会采用Index Nested-Loop Join算法(一般简写成:NLJ)

一文弄懂Join语句优化图片

一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里,通过索引匹配,取出满足条件的行,然后取出两张表的结果合集。

为了方便理解,我们会结合实验进行讲解,先来创建测试表并写入测试数据:

use martin; 
drop table if exists t1; 
CREATE TABLE `t1` (
`id` int NOT NULL auto_increment,
`a` int DEFAULT NULL,
`b` int DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
COMMENT '记录更新时间',
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


drop procedure if exists insert_t1; 


delimiter ;;
create procedure insert_t1()
begin
declare i int; 
set i=1;
while(i<=10000)do
insert into t1(a,b) values(i, i); 
set i=i+1; 
end while;
end;;
delimiter ; 
call insert_t1(); 


drop table if exists t2; 
create table t2 like t1; 
insert into t2 select * from t1 limit 100;

我们来看一个例子:

explain select * from t1 inner join t2 on t1.a = t2.a;

Tips:表 t1 和表 t2 中的 a 字段都有索引。

执行计划如下:

一文弄懂Join语句优化图片

从执行计划中可以看到这些信息:

驱动表是 t2,被驱动表是 t1。原因是:explain 分析 join 语句时,在第一行的就是驱动表;选择 t2 做驱动表的原因:如果没固定连接方式(比如没加 straight_join),优化器会优先选择小表做驱动表。所以使用 inner join 时,前面的表并不一定就是驱动表。

使用了 NLJ。原因是:一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer (***);则表示使用的 join 算法是 NLJ。

4 Block Nested-Loop Join算法

如果被驱动表的关联字段没索引,在MySQL 8.0.20版本之前,就会使用 Block Nested-Loop Join(简称:BNL)

一文弄懂Join语句优化图片

Block Nested-Loop Join(BNL) 算法的思想是:把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比,如果满足 join 条件,则返回结果给客户端。

我们一起看看下面这条 SQL 语句:

select * from t1 inner join t2 on t1.b = t2.b;

Tips:表 t1 和表 t2 中的 b 字段都没有索引

在 MySQL 5.7 版本下的执行计划如下:

一文弄懂Join语句优化图片

在 Extra 发现 Using join buffer (Block Nested Loop),这个就说明该关联查询使用的是 BNL 算法。

在 MySQL 8.0.25 版本下的执行计划如下:

一文弄懂Join语句优化图片

在 Extra 发现 Using join buffer (hash join),因为前面提到,从 MySQL 8.0.20 开始,哈希连接替换了块嵌套循环。

5 Hash Join算法

从 MySQL 8.0.20 开始,MySQL 不再使用 Block Nested-Loop Join 算法,并且在以前使用 Block Nested-Loop Join 算法的所有情况下都使用 Hash Join 优化。

一文弄懂Join语句优化图片

核心思想是将驱动表的数据加载到内存中构建哈希表

然后逐行读取被驱动表的数据,并通过哈希函数将连接条件的列的值映射为哈希值,去之前构建的Hash表查找匹配的记录

一旦在Hash表中找到匹配的记录,对这些记录进行一一比较,得出最终的Join结果

跟Block Nested-Loop Join算法类似,但是不需要将被驱动表的数据块写入内存或磁盘,更少的IO以及更节省资源

6 Batched Key Access算法

在学了 NLJ 和 BNL 算法后,你是否有个疑问,如果把 NLJ 与 BNL 两种算法的一些优秀的思想结合,是否可行呢?

比如 NLJ 的关键思想是:被驱动表的关联字段有索引。

而 BNL 的关键思想是:把驱动表的数据批量提交一部分放到 join_buffer 中。

从 MySQL 5.6 开始,确实出现了这种集 NLJ 和 BNL 两种算法优点于一体的新算法:Batched Key Access(BKA)。

一文弄懂Join语句优化图片

其原理是:

将驱动表中相关列批量放入 join_buffer 中

批量将关联字段的值发送到 Multi-Range Read(MRR) 接口

MRR 通过接收到的值,根据其对应的主键 ID 进行排序,然后再进行数据的读取和操作

返回结果给客户端。

7 补充下MRR相关知识

当表很大并且没有存储在缓存中时,使用辅助索引上的范围扫描读取行可能导致对表有很多随机访问。

而 Multi-Range Read 优化的设计思路是:查询辅助索引时,对查询结果先按照主键进行排序,并按照主键排序后的顺序,进行顺序查找,从而减少随机访问磁盘的次数。

使用 MRR 时,explain 输出的 Extra 列显示的是 Using MRR。

控制MRR的参数

optimizer_switch 中 mrr_cost_based 参数的值会影响 MRR。

如果 mrr_cost_based=on,表示优化器尝试在使用和不使用 MRR 之间进行基于成本的选择。

如果 mrr_cost_based=off,表示一直使用 MRR。

更多 MRR 信息请参考官方手册:https://dev.mysql.com/doc/refman/8.0/en/mrr-optimization.html。

8 BKA开启

先来看下这条SQL的执行计划:

explain select * from t1 inner join t2 on t1.a = t2.a;

一文弄懂Join语句优化图片

下面尝试开启 BKA :

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

这里对上面几个参数做下解释:

  • mrr=on 开启 mrr
  • mrr_cost_based=off 不需要优化器基于成本考虑使用还是不使用 MRR,也就是一直使用 MRR
  • batched_key_access=on 开启 BKA

然后再看 sql1 的执行计划:

explain select * from t1 inner join t2 on t1.a = t2.a;

一文弄懂Join语句优化图片

在 Extra 字段中发现有 Using join buffer (Batched Key Access),表示确实变成了 BKA 算法。

9 优化关联查询

扯了这么多,我们就来讲一下:究竟怎样优化关联查询:

关联字段添加索引

通过上面的内容,我们知道了 BNL、NLJ 和 BKA 的原理,因此让 BNL(Block Nested-Loop Join)或者Hash Join变成 NLJ (Index Nested-Loop Join)或者 BKA(Batched Key Access),可以提高 join 的效率。我们来看下面的例子

我们构造出两个算法对于的例子:

Block Nested-Loop Join 的例子:

select * from t1 join t2 on t1.b= t2.b;

需要 0.08 秒。

Index Nested-Loop Join 的例子:

select * from t1 join t2 on t1.a= t2.a;

只需要 0.01 秒。

再对比一下两条 SQL 的执行计划:

一文弄懂Join语句优化图片

前者扫描的行数是 100 和 9963。

对比执行时间和执行计划,再结合在本节开始讲解的两种算法的执行流程,很明显 Index Nested-Loop Join 效率更高。

因此建议在被驱动表的关联字段上添加索引,让 BNL或者Hash Join变成 NLJ 或者 BKA ,可明显优化关联查询。

选择小表作为驱动表

从上面几种Join算法,也能看出来,驱动表需要全表扫描,再存放在内存中

如果小表是驱动表,那遍历的行也会更少。

来举个例子,看下大小表做驱动表执行计划的对比:

我们来看下以 t2 为驱动表的 SQL:

select * from t2 straight_join t1 on t2.a = t1.a;

这里使用 straight_join 可以固定连接方式,让前面的表为驱动表。

再看下以 t1 为驱动表的 SQL:

select * from t1 straight_join t2 on t1.a = t2.a;

我们对比下两条 SQL 的执行计划:

一文弄懂Join语句优化图片

明显前者扫描的行数少(注意关注 explain 结果的 rows 列),所以建议小表驱动大表。

当然,如果是普通的join语句,一般不需要我们去处理,优化器默认也会选择小表做为驱动表。

数据集较大可以采用BKA优化

BKA算法采用批量处理机制,利用索引快速定位匹配记录,适合大型数据集的Join操作

版本升级

前面也提到了,如果被驱动表的关联字段没索引,在MySQL 8.0.20版本之前,就会使用 Block Nested-Loop Join(简称:BNL),

从 MySQL 8.0.20 开始,MySQL 不再使用 Block Nested-Loop Join 算法,并且在以前使用 Block Nested-Loop Join 算法的所有情况下都使用 Hash Join 优化。

相对于Block Nested-Loop Join算法,Hash Join不需要将被驱动表的数据块写入内存或磁盘,使用更少的IO以及更节省资源。

所以,假如有条件,可以升级到8.0.20之后的版本。

延伸 · 阅读

精彩推荐