4、JOIN优化策略:Nested Loop Join原理、Hash Join适用场景、驱动表选择、小表驱动大表
说到SQL优化,JOIN是绕不开的坎。很多慢查询,说白了就是JOIN没用好。我个人习惯,拿到一条慢SQL,第一件事就是看执行计划里的JOIN类型。今天咱们就把JOIN优化这块掰开揉碎讲清楚。
4.1 Nested Loop Join:最朴素的连接方式
Nested Loop Join,翻译过来就是嵌套循环连接。它的逻辑特别简单——拿驱动表的每一行,去被驱动表里找匹配的行。就像你手里有一副扑克牌,一张一张去另一副牌里找花色相同的牌。
伪代码长这样:
for each row in 驱动表:
for each row in 被驱动表:
if 匹配条件成立:
输出结果
嗯,这里要注意:被驱动表有没有索引,性能天差地别。如果被驱动表的连接列上有索引,每次查找就是一次B+树搜索,复杂度O(log n)。如果没有索引,那就是全表扫描,复杂度O(n)。
核心结论:Nested Loop Join适合小表驱动大表,且被驱动表连接列有索引的场景。驱动表越小,循环次数越少,性能越好。
我在项目中遇到过这样一个案例:一张订单表200万行,一张用户表50万行,用Nested Loop Join做关联查询,跑了30秒还没出结果。一看执行计划,被驱动表没索引。加上索引后,查询降到200毫秒。差距就是这么大。
4.2 Hash Join:大表连接的救星
Hash Join的思路完全不同。它先把驱动表的数据加载到内存,建一个哈希表。然后扫描被驱动表,每一行都去哈希表里找匹配。你想想看,哈希查找的时间复杂度是O(1),比Nested Loop的O(log n)还快。
适用场景很明确:
- 两张大表做等值连接——比如千万级表关联千万级表
- 被驱动表没有合适的索引——Nested Loop会死得很惨
- 数据分布比较均匀——哈希冲突少,性能更稳定
但Hash Join也有短板:
- 只支持等值连接(=),不支持范围连接(>、<、BETWEEN)
- 内存消耗大,如果驱动表太大,会溢出到磁盘,性能骤降
- 建哈希表本身有开销,小表场景反而比Nested Loop慢
我的建议:MySQL 8.0.18之后引入了Hash Join,如果你还在用5.7,遇到大表JOIN只能靠索引和改写SQL。升级到8.0,Hash Join能帮你省很多事。
4.3 驱动表选择:谁先谁后,差别很大
驱动表的选择,直接影响JOIN的执行效率。我见过不少开发同学,写SQL时完全不关心驱动表是谁,结果性能差了好几倍。
先看一个对比表格:
| 场景 | 驱动表 | 被驱动表 | 执行次数 | 性能 |
|---|---|---|---|---|
| 小表驱动大表 | 100行 | 100万行(有索引) | 100次索引查找 | ✅ 快 |
| 大表驱动小表 | 100万行 | 100行(有索引) | 100万次索引查找 | ❌ 慢 |
| 大表驱动大表(无索引) | 100万行 | 100万行 | 1万亿次比较 | ❌ 极慢 |
为什么会这样?因为Nested Loop Join的复杂度是O(m × n),m是驱动表行数,n是被驱动表每次查找的代价。m越小,总代价越小。
我曾经接手过一个报表系统,每天跑一次全量数据,耗时40分钟。优化前驱动表是订单明细表(500万行),被驱动表是商品表(2万行)。我改成商品表做驱动表,订单明细表做被驱动表,加上索引,跑完只要3分钟。嗯,这就是小表驱动大表的威力。
4.4 小表驱动大表:不只是理论
「小表驱动大表」这个原则,说起来简单,做起来有讲究。什么叫小表?不是看物理行数,而是看参与JOIN的数据量。
举个例子:
-- 场景A:直接关联
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_date > '2024-01-01';
-- 场景B:先过滤再关联
SELECT * FROM (
SELECT * FROM orders
WHERE create_date > '2024-01-01'
) o
JOIN users u ON o.user_id = u.id;
场景A里,驱动表可能是users(50万行),被驱动表是orders(200万行)。但场景B里,子查询先过滤了orders,可能只剩10万行,这时候orders反而成了小表。
避坑指南:我曾经犯过一个错误——以为物理行数少的表就是小表。结果有一张配置表只有100行,但每条配置关联了上百万条业务数据。用配置表做驱动表,Nested Loop跑了100次,每次都要扫描百万行,反而更慢。后来改成业务表做驱动表,配置表建索引,一次扫描搞定。
判断小表的正确姿势:
- 看过滤后的数据量,不是原始行数
- 看连接列的基数,基数高的列做驱动表更优
- 看是否走索引,被驱动表有索引时,驱动表越小越好
4.5 三种JOIN方式对比总结
| 特性 | Nested Loop Join | Hash Join | Sort Merge Join |
|---|---|---|---|
| 适用场景 | 小表驱动大表,被驱动表有索引 | 大表等值连接,无索引或索引差 | 大表排序后连接,范围查询 |
| 连接类型 | 所有类型 | 仅等值 | 等值+范围 |
| 内存消耗 | 低 | 高(需建哈希表) | 中(需排序) |
| 索引依赖 | 强依赖 | 不依赖 | 不依赖 |
| MySQL支持 | 所有版本 | 8.0.18+ | 不支持(InnoDB) |
4.6 核心逻辑流程图
下面这张图,把三种JOIN的执行流程和选择逻辑串起来了。我建议你保存下来,写SQL前看一眼,能少踩很多坑。
4.7 实战建议
最后,给你几条我踩过坑之后总结的实战建议:
- 先看执行计划——用EXPLAIN查看JOIN类型和驱动表,别靠猜
- 被驱动表连接列必须建索引——这是Nested Loop Join的命门
- 能用Hash Join就别用Nested Loop——MySQL 8.0+优先考虑Hash Join
- 过滤条件提前下推——子查询或CTE先过滤,减少JOIN数据量
- 避免笛卡尔积——忘记写ON条件,两张表直接相乘,那是灾难
一个小技巧:如果你不确定驱动表是谁,可以在EXPLAIN输出里看第一行——MySQL一般会把驱动表放在第一行。如果发现大表在前面,可以考虑用STRAIGHT_JOIN强制指定驱动表顺序。