一、结论(一句话先说透)

大表 JOIN 小表时,最关键的原则是让“小表驱动大表”,并确保 JOIN 字段可走索引。

注意:驱动表不是选“物理上小的表”,而是选“过滤后参与 JOIN 最小的表”。

否则会导致:

  • 大表被重复扫描
  • JOIN 触发全表 Nested Loop
  • 严重的临时表 / 回表 / 大量随机 IO
  • 甚至出现 “Using join buffer” 影响性能

二、核心原则:小表驱动大表

数据库 JOIN 本质上是:

驱动表(外层循环) × 被驱动表(内层查找)

等价于:

for row in 驱动表:
    去被驱动表查找匹配行(通常走索引)

为什么必须让小表做驱动表?

假设:

  • 大表:1000 万行
  • 小表:1 万行

无论如何:

小表 → 大表:10000 次查找
大表 → 小表:10000000 次查找(极慢)

甚至差了两个数量级。

这就是优化 JOIN 的核心原因。


三、MySQL(特别是 InnoDB)中的 JOIN 规则

1、小表驱动大表(非常重要)

MySQL 采用 Nested Loop Join(嵌套循环),不是 Hash Join。

伪代码:

for row in 驱动表:
    在被驱动表中根据 join 字段查找(要靠索引)

因此:

  • 驱动表越小越好
  • 被驱动表 join 字段必须有索引

2、优化器选的驱动表不一定最优

MySQL 5.7 / 8.0 的优化器会自动决定驱动表
但它不一定最聪明(统计信息不准时容易选错)

因此我们可以通过:

  • STRAIGHT_JOIN 强制顺序
  • 提供更好的索引
  • 改写 SQL(写小表在前)
  • 更新统计信息

来影响驱动表选择。


四、JOIN 写法对执行计划的影响

1、小表放前面(较强的暗示作用)

SELECT *
FROM small_table s
JOIN big_table b ON s.id = b.sid;

虽然不是强制,但较容易让优化器选择 small_table 为驱动表。


2、使用 STRAIGHT_JOIN 强制执行顺序

SELECT STRAIGHT_JOIN *
FROM small_table s
JOIN big_table b ON s.id = b.sid;

表示:s 一定驱动 b

大表不会再误当驱动表扫描上千万次。


五、JOIN 字段必须有索引

大多数 JOIN 性能问题来自:

  • 被驱动表 join 字段无索引
  • 导致 MySQL 必须扫描整个被驱动表
  • 从而出现 “Using join buffer”

举例:

SELECT *
FROM small s
JOIN big b ON s.user_id = b.user_id;

如果 big.user_id 没有索引

for s 每一行:
    full scan big(1000万行)

严重灾难。

因此 JOIN 的被驱动表(通常是大表)的 join 字段必须有索引。


六、大表 JOIN 小表的优化套路

1、确保 join 字段有索引(被驱动表)

方式:

ALTER TABLE big ADD INDEX idx_userid(user_id);

2、过滤尽量放在驱动表

例子:

WHERE small.status = 1

让驱动表更小 → 循环次数更少。


3、尽量减少 JOIN 字段的数据类型不一致

否则 Index 可能失效:

  • int join varchar
  • varchar join char
  • char join varchar(30)

4、避免复杂计算参与 join

例如:

ON DATE(big.create_time) = small.date

这样完全用不了索引,应改成:

ON big.create_time BETWEEN small.date AND small.date + INTERVAL 1 DAY

5、尽量避免 JOIN 大文本字段(浪费 IO)

例如:

  • TEXT
  • JSON
  • BLOB

这些应使用投影(SELECT 某些字段)。


七、案例

场景

A 表(1 万行)
B 表(2000 万行)

SQL:

SELECT *
FROM A
JOIN B ON A.uid = B.uid
WHERE A.status = 1;

执行计划发现:

  • B 做驱动表(非常糟糕)
  • B.uid 没索引
  • 扫描 2000 万 × 1 万 = 2 × 10¹¹ 次操作(直接高 CPU)

优化后:

ALTER TABLE B ADD INDEX idx_uid(uid);

或强制写法:

SELECT STRAIGHT_JOIN *
FROM A
JOIN B ON A.uid = B.uid
WHERE A.status = 1;

执行计划变成:

  • A 为驱动表
  • B 索引查找
  • 性能从分钟级 → 毫毫秒级

八、总结

1、小表驱动大表

Nested Loop 的特性决定了 join 时必须尽量让小表做外层循环。

2、join 字段必须有索引

尤其是被驱动表,否则就是灾难性全表扫描。

3、让优化器更容易选择正确的执行计划

  • 小表放前面
  • STRAIGHT_JOIN
  • 保证索引可用
  • 避免函数/类型转换

4、过滤条件尽量放在驱动表

让循环次数更少。

5、关注执行计划(EXPLAIN)

是否出现:

  • Using join buffer(警告)
  • 全表扫描
  • 索引未使用
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐