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

循环驱动表中的每一行
再到被驱动表找到满足关联条件的记录
因为关联字段没索引,所以在被驱动表里的查询需要全表扫描
这种方法逻辑简单,但是效率很差
比如驱动表数据量是 m,被驱动表数据量是 n,则扫描行数为 m * n
当然,好在,MySQL也没有采用这种算法,即使关联字段没索引,也会采用Block Nested-Loop Join或者Hash Join,等下会细说。
3 Index Nested-Loop Join算法
刚才我们说的是关联字段没索引的情况,假如关联字段有索引,就会采用Index Nested-Loop Join算法(一般简写成:NLJ)

一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里,通过索引匹配,取出满足条件的行,然后取出两张表的结果合集。
为了方便理解,我们会结合实验进行讲解,先来创建测试表并写入测试数据:
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_TIMESTAMPCOMMENT '记录更新时间',PRIMARY KEY (`id`),KEY `idx_a` (`a`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;drop procedure if exists insert_t1;delimiter ;;create procedure insert_t1()begindeclare i int;set i=1;while(i<=10000)doinsert 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 字段都有索引。
执行计划如下:

从执行计划中可以看到这些信息:
驱动表是 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)

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 版本下的执行计划如下:

在 Extra 发现 Using join buffer (Block Nested Loop),这个就说明该关联查询使用的是 BNL 算法。
在 MySQL 8.0.25 版本下的执行计划如下:

在 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 优化。

核心思想是将驱动表的数据加载到内存中构建哈希表
然后逐行读取被驱动表的数据,并通过哈希函数将连接条件的列的值映射为哈希值,去之前构建的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_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;

下面尝试开启 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;

在 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 的执行计划:

前者扫描的行数是 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 的执行计划:

明显前者扫描的行数少(注意关注 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之后的版本。
2 月 9 日消息,雷神 (THUNDEROBOT) 现已宣布推出基于英
2 月 10 日消息,制造商 Musnap 现已在海外推出一款 Oce
一文弄懂Join语句优化
26-03-01空间都去哪里了?(中)
26-03-01数据库死锁排查思路分享(纠正篇)
26-03-01在启用所有PS性能模式工具的情况下,性能损耗过高,怎么办
26-03-01备库是否有自己的统计信息?(续)
26-03-0110个大大提升MySQL效率的使用技巧
26-03-01TiDB 7.x 源码编译之 TiDB Server 篇,及新特性详解
26-03-01mysql中一条查询语句的执行全过程是怎样的?
26-03-01