小表驱动大表是嵌套循环连接下避免指数级扫描的唯一合理选择;关键在于过滤后行数最少的表作驱动表,且被驱动表关联字段必须有匹配索引,否则即使小表驱动仍会引发海量扫描。

不是“必须”,而是嵌套循环连接(Nested Loop Join)下,小表驱动大表是唯一能避免指数级扫描的合理选择;MySQL 5.7/8.0 默认不支持 Hash Join,所以你写的 JOIN 几乎必然走嵌套循环。
驱动表选错会导致总扫描行数爆炸
数据库执行 JOIN 的伪代码本质就是两层循环:
for row in 驱动表:
在被驱动表中根据 ON 字段查找匹配行(靠索引才快)
关键变量只有两个:外层循环次数(= 驱动表过滤后行数)、内层单次查找成本(= 是否走索引 + 索引深度)。
如果驱动表过滤后有 50 万行,被驱动表有 1000 万行且 ON 字段无索引:总扫描量 ≈ 50 万 × 1000 万 = 50 万亿行 —— 这不是慢,是根本跑不完。
常见错误现象:
-
EXPLAIN显示某张大表的rows列高达几十万,但它却被选为驱动表 - 执行时出现
Using join buffer (Block Nested Loop),说明内层无法走索引,退化成块嵌套循环 - 查询耗时从毫秒级跳到分钟级,且
IO Wait占比极高
“小表”不是指建表时的物理大小
优化器看的是 WHERE 过滤后的预估行数,不是 SHOW TABLE STATUS 里的 Data_length。一张 200 万行的订单表,加了 WHERE status IN (1,2,3) 后只剩 3 万行,它就可能成为实际驱动表。
容易踩的坑:
- 没跑
ANALYZE TABLE,统计信息过期,优化器误判过滤后行数 - 在
WHERE条件里用了函数(如DATE(create_time) = '2026-09-01'),导致索引失效、过滤失效、优化器误以为“全表都要扫” - 把
LEFT JOIN的右表当“可选小表”,却忘了LEFT JOIN强制左表为驱动表 —— 即使左表过滤后仍有 100 万行,也得硬着头皮驱动
被驱动表的 ON 字段没索引,小表驱动毫无意义
小表只驱动 100 行,但如果被驱动表的关联字段没索引,每次匹配都得全表扫描 1000 万行 → 总扫描量 = 100 × 1000 万 = 10 亿行。
实操建议:
- 对每个
ON子句右侧的字段,单独建索引:ALTER TABLE large_table ADD INDEX idx_join_col (join_col) - 联合索引要注意顺序:如果
ON t1.a = t2.x AND t1.b = t2.y,索引必须是INDEX(x, y),不能是INDEX(y, x) - 检查字段类型是否完全一致:
INT和UNSIGNED INT、VARCHAR(50)和VARCHAR(100)都会触发隐式转换,让索引失效
怎么确认当前 SQL 是不是小表驱动?
别看 FROM 后面谁写在前面,要看 EXPLAIN 输出里每行的 rows 值最小的那个表 —— 它才是真正的驱动表。
如果发现不是你预期的小结果集,优先做三件事:
- 对涉及的表执行
ANALYZE TABLE table_name - 把大表的过滤条件尽量前移到子查询里:
SELECT * FROM (SELECT id FROM large_table WHERE status = 1) t JOIN small_table s ON t.id = s.large_id - 确认所有
ON字段都有索引,且类型严格一致
真正卡住性能的,从来不是“要不要小表驱动”,而是“你认定的小表,在过滤后还小吗?”和“被驱动表那列,到底有没有走索引?”——这两个问题不钉死,其他所有技巧都是表面功夫。











