避免 JOIN 灾难:为什么必须“小表驱动大表”?
摘要: 大表JOIN小表时,核心原则是小表驱动大表且JOIN字段需有索引,否则易引发性能问题(如全表扫描、大量随机IO)。MySQL采用嵌套循环(Nested Loop),驱动表越小、循环次数越少;被驱动表若无索引会导致灾难性扫描。优化方法包括:强制小表驱动(STRAIGHT_JOIN)、确保JOIN字段索引、过滤条件前置、避免数据类型不一致及复杂计算。案例显示,优化后性能可从分钟级提升至毫秒级。
文章目录
一、结论(一句话先说透)
大表 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(警告)
- 全表扫描
- 索引未使用
更多推荐


所有评论(0)