为什么你的JOIN查询慢?驱动表选对了,但被驱动表没索引

简介: 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';

输出中,如果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 不愁

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

相关文章
|
1月前
|
存储 关系型数据库 MySQL
对账差了三毛钱,查完我把全部金额字段从DOUBLE改成了DECIMAL
一次财务对账差三毛钱的排查,牵出金额字段用浮点数的老坑。从IEEE 754为什么存不准0.1讲起,用同一批金额把FLOAT、DOUBLE、DECIMAL三种类型实测对比,再给出金额字段的选型、聚合与改表做法,附避坑清单。
|
1月前
|
缓存 人工智能 关系型数据库
大模型调用成本降62%?语义缓存的阈值与命中率实测
客服机器人上线两周,用户问题高度重复,每次都走完整套RAG,数据库读QPS翻三倍,大模型账单飞涨。文章讲清语义缓存怎么用"向量相似度"代替"字符串相等"去命中重复提问,落地时数据该存哪、相似度阈值怎么定,以及多租户隔离、知识库更新失效、别缓存低质量回答这几个真正的难点。附两周实测:命中率约57%,大模型成本降约62%,命中时首字延迟从3.2秒降到0.45秒。
|
1月前
|
存储 OLAP OLTP
从OLTP到OLAP到HTAP:数据库负载分类的技术演进与选型指南
OLTP和OLAP是数据库最常被混淆的一对概念。很多人知道“OLTP是交易,OLAP是分析”,但不清楚它们在存储结构、索引设计、执行引擎上的根本差异。本文从数据组织方式、查询模式、优化目标三个层面,拆解OLTP和OLAP的本质区别,并结合实际使用场景给出选型建议。
|
1月前
|
SQL 算法 关系型数据库
MySQL大表DDL锁表避坑指南:COPY、INPLACE、INSTANT三种算法全解析
很多人只知道“Online DDL不锁表”,却不清楚什么场景真的不锁、什么场景照样锁。本文从DDL的三种算法入手,拆解Online DDL的触发条件、锁表场景、以及大表DDL的实战避坑方案。
|
1月前
|
SQL 运维 算法
订单表上亿行,我按用户ID拆成128片之后怎么样了
从单表几千万行慢查询的痛点出发,讲清垂直拆分与水平拆分的区别、分片键怎么选、分片算法(hash取模/range/一致性hash)怎么权衡,以及分库分表带来的分布式ID、跨片查询、分布式事务等问题,给出避免过度拆分的避坑清单。
|
1月前
|
SQL 监控 关系型数据库
MySQL索引合并优化器陷阱:为什么复合索引比索引合并快一个数量级?
MySQL优化器有一个“自作聪明”的行为——当单列索引无法完全覆盖查询时,它可能选择索引合并(Index Merge) ,同时使用多个单列索引,把结果集合并起来。听起来很合理对吧?但索引合并有严格的适用条件,用错了比全表扫描还慢——尤其是UNION类型的索引合并,需要对多个结果集去重和排序,代价极高。本文拆解索引合并的3种类型、3个踩坑场景,以及什么时候该用复合索引替代。
|
1月前
|
SQL 关系型数据库 MySQL
别再盯着EXPLAIN的rows列了,8.0.18之后有更好的选择
EXPLAIN是DBA最常用的工具之一,但大多数人还在看type、rows、Extra这些传统字段——然后靠经验猜。MySQL 8.0.18开始引入的EXPLAIN ANALYZE,直接把实际执行时间和行数输出给你看,不用猜了。本文对比传统EXPLAIN和EXPLAIN ANALYZE的差异,展示如何用新工具把执行计划分析这件事从“猜”变成“看”。
|
1月前
|
机器学习/深度学习 前端开发
发动机故障诊断智能体(三):特征可分性优化与加权对比投影层
在基础时序编码网络完成对正常工况的压缩表征后,尽管模型具备了提取稳态规律的能力,但在面对多种不同类型的微量气路故障时,潜在空间中的特征分布仍可能存在相互交叠的现象。该组件致力于通过引入度量对比学习机制,在特定的低维投影空间内重塑特征拓扑结构,系统性优化不同故障模式之间的几何可分性。
|
1月前
|
人工智能 Cloud Native 数据库
向量数据库选型实战:从 Embedding、ANN 索引到三条落地路线
本文直击向量数据库本质:不堆概念,不列产品,专讲它“是什么”、三条选型路径(专用库/关系库扩展/云托管)如何取舍,以及开发者落地必须关注的召回率、延迟、更新一致性等真实问题。聚焦RAG实战,强调“先用pgvector跑通再升级”,拒绝盲目上马。
153 0
|
1月前
|
SQL 人工智能 数据库
AI写的SQL语法对、性能炸?上线前五道关卡能救命
从AI生成SQL的三大翻车模式(字段幻觉、性能灾难、语义错误)出发,给出上线前五道审核关卡:结构预检、执行计划校验、高危操作拦截、灰度上线、审计追踪,附SQL示例与避坑清单。