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前看一眼,能少踩很多坑。

JOIN优化策略核心逻辑 收到JOIN查询 连接条件是否为等值? 是 否 Hash Join 建哈希表 → 探测匹配 适合:大表等值连接 Nested Loop Join 驱动表循环 → 被驱动表查找 适合:小表驱动大表+索引 驱动表选择:小表驱动大表 最优JOIN策略:小表+索引+等值 💡 核心原则:先过滤再JOIN,小表做驱动,被驱动表建索引,等值连接优先用Hash Join

4.7 实战建议

最后,给你几条我踩过坑之后总结的实战建议:

  1. 先看执行计划——用EXPLAIN查看JOIN类型和驱动表,别靠猜
  2. 被驱动表连接列必须建索引——这是Nested Loop Join的命门
  3. 能用Hash Join就别用Nested Loop——MySQL 8.0+优先考虑Hash Join
  4. 过滤条件提前下推——子查询或CTE先过滤,减少JOIN数据量
  5. 避免笛卡尔积——忘记写ON条件,两张表直接相乘,那是灾难

一个小技巧:如果你不确定驱动表是谁,可以在EXPLAIN输出里看第一行——MySQL一般会把驱动表放在第一行。如果发现大表在前面,可以考虑用STRAIGHT_JOIN强制指定驱动表顺序。