MySQL 8.0.20移除了Block Nested Loop,之前学的JOIN优化知识还适用吗?

举报
这个DBA有点耶 发表于 2026/09/17 17:08:37 2026/09/17
【摘要】 MySQL的JOIN优化中,驱动表的选择直接决定执行效率。“小表驱动大表”这句口诀几乎人人都会背,但真正理解其底层逻辑的人不多——驱动表看的是过滤后结果集,不是表的总行数;Hash Join引入后,Join Buffer的角色也发生了根本变化。本文从驱动表选择逻辑、Join Buffer工作机制、MySQL 8.0.20之后的算法演进三个层面,拆解多表JOIN的底层原理与实战调优方法。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

“小表驱动大表”——这句话你肯定听过。

但上周一个朋友发来一条JOIN查询,两张表:orders表200万行,users表50万行。他说:“users表小,应该是驱动表吧?为什么EXPLAIN显示orders是驱动表,查询还跑了8秒?”

我看了眼他的SQL:

SELECT o.order_id, u.username, o.amount
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_time >= '2026-09-01'
  AND u.status = 'ACTIVE';

我问他:“orders表200万行,加了create_time条件后剩多少行?users表50万行,加了status条件后剩多少行?”

他愣了一下,查了一下:orders过滤后只剩3000行,users过滤后还有40万行。

驱动表选的是orders,没选错。

“小表驱动大表”里的“小表”,看的是WHERE条件过滤后的结果集,不是表的总行数。这个误解,我在技术群里见过太多次了。

今天把驱动表选择和Join Buffer这件事彻底拆开讲清楚。

一、驱动表到底怎么选的?

JOIN的本质是嵌套循环:从一张表循环取出数据,拿着关联字段去另一张表中匹配。

  • 驱动表:外层循环的表,是数据遍历的起点

  • 被驱动表:内层循环的表,是每次循环匹配的对象

优化器选择驱动表的核心逻辑是:经过WHERE条件过滤后,结果集行数最少的表作为驱动表

注意,这里的关键词是“过滤后”。一张200万行的表和一张50万行的表,谁当驱动表,取决于WHERE条件把哪张表过滤得更小。

不同JOIN类型的驱动表选择规则:

JOIN类型 驱动表选择规则
INNER JOIN 优化器根据过滤后结果集大小自动选择
LEFT JOIN 左表为驱动表(除非WHERE条件强制过滤右表,可能转为INNER JOIN)
RIGHT JOIN 右表为驱动表(同理)

一个关键细节:LEFT JOIN的左表默认是驱动表,但如果WHERE条件中包含了对右表的强制过滤条件,优化器可能将LEFT JOIN转为INNER JOIN,重新选择驱动表。这一点很多人不知道。

那怎么确认优化器选了谁当驱动表?看EXPLAIN输出:id值相同的行,table列排在上面的就是驱动表

EXPLAIN SELECT o.order_id, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_time >= '2026-09-01' AND u.status = 'ACTIVE';

输出中,如果ordersidusers一样,但orders排在users上面,说明orders是驱动表。

二、Join Buffer是什么?

当被驱动表的关联字段没有索引时,MySQL无法使用Index Nested Loop(索引嵌套循环连接)。这时候,如果每取一行驱动表的数据就去全表扫描一次被驱动表,效率会极低。

Join Buffer的引入就是为了解决这个问题。

它的工作方式是:把驱动表的一批数据先缓存到内存中,然后一次性拿这批数据去匹配被驱动表,减少被驱动表的全表扫描次数。

举个例子:驱动表有1000行,被驱动表有100万行。没有Join Buffer,需要扫描被驱动表1000次。有了Join Buffer,假设每次缓存100行驱动表数据,只需要扫描被驱动表10次。扫描次数从1000次降到10次,提升100倍。

但有一个关键细节很多人不知道:Join Buffer缓存的是查询列表中所有需要的列,不只是关联字段。这意味着SELECT *会让Join Buffer占用大量内存,更快触发溢出。只查询必要的列,能显著提升Join Buffer的利用效率。

join_buffer_size默认值只有256KB,最大可设置为4GB。 但这个参数的调整需要谨慎。

三、MySQL 8.0.20的重大变化:Block Nested Loop被移除

这是本文最重要的一部分。

在MySQL 8.0.20之前,无索引的JOIN使用Block Nested Loop(BNL) 算法,Join Buffer是BNL的核心组件。

从MySQL 8.0.20开始,BNL算法被正式移除,Hash Join成为无索引JOIN的唯一算法

这意味着什么?

Hash Join不使用Join Buffer。

Hash Join的工作方式是:选择结果集较小的表作为构建表(build table),在内存中构建哈希表;然后用另一张表(探测表)的每一行去哈希表中探测匹配。

哈希表的大小由join_buffer_size控制,但和BNL的用法完全不同。

  • BNL的Join Buffer:缓存驱动表的数据行,用于减少被驱动表的扫描次数

  • Hash Join的哈希表:存储构建表的关联键和需要的列,用于O(1)探测

这是两个完全不同的概念。在MySQL 8.0.20+的环境中,join_buffer_size的作用对象已经从“Join Buffer”变成了“哈希表”。

版本差异总结:

MySQL版本 无索引JOIN算法 join_buffer_size的作用
5.7及以下 Block Nested Loop 缓存驱动表数据行
8.0.18-8.0.19 Hash Join(实验性) 哈希表大小
8.0.20+ Hash Join(唯一) 哈希表大小

四、Join Buffer(哈希表)调优实战

调优原则一:先确认是否真的需要调大

在MySQL 8.0.20+中,如果EXPLAIN显示Using join buffer (hash join),说明优化器选择了Hash Join。但哈希表不一定需要很大——如果构建表的结果集本身就很小(比如几千行),默认的256KB可能就够了。

只有当构建表结果集较大、哈希表溢出到磁盘时,才需要调大。

调优原则二:单连接不要超过1MB

join_buffer_size每个连接独立分配的。如果有100个并发连接,每个连接的哈希表都是1MB,总内存消耗就是100MB。调得太大,高并发场景下容易OOM。

生产环境建议:单连接不超过1MB,全局不超过10MB。对于OLTP场景,保持较小值(256KB-512KB)更安全;批处理类场景可以适当增大。

调优原则三:优先加索引,而不是调参数

最根本的优化是让被驱动表的关联字段有索引。有索引时,JOIN走Index Nested Loop,不需要Hash Join,也就不需要大哈希表。

索引是JOIN优化的第一原则,参数调优只是权宜之计。

五、小结

JOIN优化的核心不在于调大join_buffer_size,而在于两件事:一是让优化器选对驱动表,二是让被驱动表的关联字段有索引。驱动表看的是过滤后结果集,不是表的总行数。MySQL 8.0.20之后Block Nested Loop被移除,Hash Join成为无索引JOIN的唯一算法,join_buffer_size的作用也从“缓存驱动表数据”变成了“控制哈希表大小”。理解这些底层变化,才能写出真正高效的JOIN查询。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。