写点什么

深入理解 MySQL 中的 Join 算法

作者:码农BookSea
  • 2023-10-07
    浙江
  • 本文字数:2983 字

    阅读完需:约 10 分钟

深入理解MySQL中的Join算法

本文已收录至 GitHub,推荐阅读 👉 Java随想录

微信公众号:Java 随想录


原创不易,注重版权。转载请注明原作者和原文链接


在数据库处理中,Join 操作是最基本且最重要的操作之一,它能将不同的表连接起来,实现对数据集的更深层次分析。

MySQL 作为一款流行的关系型数据库管理系统,其在执行 Join 操作时使用了多种高效的算法,包括 Index Nested-Loop Join(NLJ)和 Block Nested-Loop Join(BNL)。这些算法各有优缺点,本文将探讨这两种算法的工作原理,以及如何在 MySQL 中使用它们。

什么是 Join

在 MySQL 中,Join 是一种用于组合两个或多个表中数据的查询操作。Join 操作通常基于两个表中的某些共同的列进行,这些列在两个表中都存在。MySQL 支持多种类型的 Join 操作,如 Inner JoinLeft JoinRight Join 等。

Inner Join 是最常见的 Join 类型之一。在 Inner Join 操作中,只有在两个表中都存在的行才会被返回。

例如,如果我们有一个“customers”表和一个“orders”表,我们可以通过在这两个表中共享“customer_id”列来组合它们的数据。

SELECT *FROM customersINNER JOIN ordersON customers.customer_id = orders.customer_id;
复制代码

上面的查询将返回所有存在于“customers”和“orders”表中的“customer_id”列相同的行。

Index Nested-Loop Join

Index Nested-Loop Join(NLJ)算法是 Join 算法中最基本的算法之一。

在 NLJ 算法中,MySQL 首先会选择一个表(通常是小型表)作为驱动表,并迭代该表中的每一行。然后,MySQL 在第二个表中搜索匹配条件的行,这个搜索过程通常使用索引来完成。一旦找到匹配的行,MySQL 将这些行组合在一起,并将它们作为结果集返回。

工作流程如图:

例如,执行下面这个语句:

select * from t1 straight_join t2 on (t1.a=t2.a);
复制代码

注:当使用 straight_join 时,MySQL 会强制按照在查询中指定的从左到右的顺序执行连接。

在这个语句里,假设 t1 是驱动表,t2 是被驱动表。我们来看一下这条语句的 explain 结果。

可以看到,在这条语句里,被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,因此这个语句的执行流程是这样的:

  1. 从表 t1 中读入一行数据 R;

  2. 从数据行 R 中,取出 a 字段到表 t2 里去查找;

  3. 取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;

  4. 重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。

这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为「Index Nested-Loop Join」,简称 NLJ

NLJ 是使用上了索引的情况,那如果查询条件没有使用到索引呢?

MySQL 会选择使用另一个叫作「Block Nested-Loop Join」的算法,简称 BNL

Block Nested-Loop Join

Block Nested Loop Join(BNL)算法与 NLJ 算法不同的是,BNL 算法使用一个类似于缓存的机制,将表数据分成多个块,然后逐个处理这些块,以减少内存和 CPU 的消耗。

例如,执行下面这个语句:

select * from t1 straight_join t2 on (t1.a=t2.b);
复制代码

如果 t2 表的字段 b 上是没有建立索引的。这时候,被驱动表上没有可用的索引,算法的流程是这样的:

  1. 把表 t1 的数据读入线程内存join_buffer中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;

  2. 扫描表 t2,把表 t2 中的每一行取出来,跟join_buffer中的数据做对比,满足 join 条件的,作为结果集的一部分返回。

这条 SQL 语句的 explain 结果如下所示:

可以看到,在这个过程中,MySQL 对表 t1 和 t2 都做了一次全表扫描,因此总的扫描行数是 1100。

由于join_buffer是以无序数组的方式组织的,因此对表 t2 中的每一行,都要做 100 次判断,总共需要在内存中做的判断次数是:100*1000=10 万次

虽然 Block Nested-Loop Join 算法是全表扫描。但是是在内存中进行的判断操作,速度上会快很多。但是性能仍然不如 NLJ。

join_buffer的大小是由参数 join_buffer_size 设定的,默认值是 256k。

那如果 join_buffer_size 的大小不足以放下表 t1 的所有数据呢?

办法很简单,就是分段放,执行流程如下:

  1. 顺序读取数据行放入join_buffer中,直到join_buffer满了。

  2. 扫描被驱动表跟join_buffer中的数据做对比,满足 join 条件的,作为结果集的一部分返回。

  3. 清空join_buffer,重复上述步骤。

虽然分成多次放入join_buffer,但是判断等值条件的次数还是不变的,依然是 10 万次。

MRR & BKA

上篇文章里我们有提到 MRR(Multi-Range Read)。MySQL 在 5.6 版本后引入了**Batched Key Acess(BKA)**算法,这个 BKA 算法,其实就是对 NLJ 算法的优化,而 BKA 算法正是基于 MRR。

NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了

其实我们可以从表 t1 里一次性地多拿些行出来,先放到一个临时内存,一起传给表 t2。这个临时内存不是别人,就是join_buffer

通过上一篇文章,我们知道join_buffer 在 BNL 算法里的作用,是暂存驱动表的数据。但是在 NLJ 算法里并没有用。那么,我们刚好就可以复用join_buffer到 BKA 算法中。

NLJ 算法优化后的 BKA 算法的流程,如图所示:

图中,在join_buffer中放入的数据是 R1~R100,表示的是只会取查询需要的字段。当然,如果join buffer放不下 R1~R100 的所有数据,就会把这 100 行数据分成多段执行上图的流程。

如果要使用 BKA 优化算法的话,你需要在执行 SQL 语句之前,先设置

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
复制代码

其中,前两个参数的作用是要启用 MRR。这么做的原因是,BKA 算法的优化要依赖于 MRR。

对于 BNL,我们可以通过建立索引转为 BKA。但是,有时候你确实会碰到一些不适合在被驱动表上建索引的情况。比如下面这个语句:

select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;
复制代码

假设 t1 表 1000 行,t2 表 100 万行,t2.b<=2000 过滤后,t2 表需要参与 join 的只有 2000 行数据。

如果这条语句是一个低频的 SQL 语句,那么在表 t2 的字段 b 上创建索引就很浪费了。

这时候,我们可以考虑使用临时表。使用临时表的大致思路是:

  1. 把表 t2 中满足条件的数据放在临时表 tmp_t 中;

  2. 为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;

  3. 让表 t1 和 tmp_t 做 join 操作。

此时,对应的 SQL 语句的写法如下:

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;insert into temp_t select * from t2 where b>=1 and b<=2000;select * from t1 join temp_t on (t1.b=temp_t.b);
复制代码

总体来看,不论是在原表上加索引,还是用有索引的临时表,我们的思路都是让 join 语句能够用上被驱动表上的索引,来触发 BKA 算法,提升查询性能。

总结

在 MySQL 中,不管 Join 使用的是 NLJ 还是 BNL 总是应该使用小表做驱动表。更准确地说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表

另外应当尽量避免使用 BNL 算法,如果确认优化器会使用 BNL 算法,就需要做优化。优化的常见做法是,给被驱动表的 join 字段加上索引,把 BNL 算法转成 BKA 算法。对于不好在索引的情况,可以基于临时表的改进方案,提前过滤出小数据添加索引。


感谢阅读,如果本篇文章有任何错误和建议,欢迎给我留言指正。

老铁们,关注我的微信公众号「Java 随想录」,专注分享 Java 技术干货,文章持续更新,可以关注公众号第一时间阅读。

一起交流学习,期待与你共同进步!

发布于: 刚刚阅读数: 2
用户头像

码农BookSea

关注

Java开发工程师 2021-12-26 加入

Java开发菜鸟工程师,写博客的初衷是为了沉淀我所学习,累积我所见闻,分享我所体验。希望和更多的人交流学习。

评论

发布
暂无评论
深入理解MySQL中的Join算法_Java_码农BookSea_InfoQ写作社区