MySQL 8.0.20移除了Block Nested Loop,之前学的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';
输出中,如果orders的id和users一样,但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 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~
- 点赞
- 收藏
- 关注作者
评论(0)