MySQL优先让小表驱动大表,因其默认Nested-Loop Join算法使驱动表行数直接决定内层查找次数,小表驱动可显著降低I/O与CPU开销。

MySQL 优先让小表驱动大表,不是为了“语法好看”或“约定俗成”,而是因为它的默认 JOIN 算法是 Nested-Loop Join,外层循环次数直接决定总开销——10 行小表驱动,最多触发 10 次内层查找;100 万行大表驱动,就是百万次查找,哪怕每次只走一次索引,I/O 和 CPU 成本也完全不在一个量级。
为什么Nested-Loop Join天然偏爱小驱动表
MySQL(8.0.18 前默认)执行 JOIN 的核心逻辑是:
- 取驱动表一行 → 根据
ON条件去被驱动表查匹配行 → 返回结果 - 重复直到驱动表遍历完
这个过程里,驱动表的行数 = 内层查找的调用次数。而被驱动表是否能快速响应,取决于它有没有索引。但无论索引多高效,100 万次调用和 10 次调用的调度、上下文切换、缓冲区管理成本差异巨大。
常见错误现象:EXPLAIN 显示大表的 type 是 ALL 或 index,且 rows 列高达几十万 —— 这说明它被当成了驱动表,哪怕你写的是 small_table JOIN big_table,优化器也可能因统计不准把它选错。
EXPLAIN 中哪一列真正告诉你谁是驱动表
别看 FROM 子句顺序,也别猜“左表就是驱动表”——在 INNER JOIN 下,优化器会重排。真正可靠的是每行输出里的 rows 列:
-
rows值最小的那张表,大概率是驱动表(优化器估算的过滤后行数) -
type = ALL或type = index且rows很大 → 它是被全扫的驱动表(危险信号) -
type = ref或eq_ref且key非空 → 它是被索引查找的被驱动表(理想状态)
注意:LEFT JOIN 是例外:左表强制为驱动表,EXPLAIN 再准也没法绕过语法限制。如果你看到 LEFT JOIN big_table ON ... 且左表 rows 很大,优化方向不是换顺序,而是加 WHERE 提前过滤左表,或者改写成 INNER JOIN(如果语义允许)。
小表驱动失效的三个典型坑
即使你把小表放前面,也可能白搭:
-
被驱动表缺索引:比如
JOIN orders o ON u.id = o.user_id,但orders.user_id没索引 → 小表驱动 10 行,大表仍要扫 10 × 100 万行 -
字段类型不一致:
users.id是INT,orders.user_id是UNSIGNED INT或VARCHAR→ 触发隐式转换,索引失效,退化成全表扫描 -
统计信息过期:表数据变了但没跑
ANALYZE TABLE→ 优化器以为某表只有 100 行,实际已涨到 50 万,选错驱动表
复合 ON 条件更敏感:ON t1.a = t2.x AND t1.b = t2.y 要求 t2 上有 INDEX(x, y),顺序不能颠倒;INDEX(a, b) 对 ON t2.b = ? 完全无效。
什么时候可以不管“小表驱动”
不是所有场景都死守这条规则:
- MySQL 8.0.19+ 启用了
hash_join=on,优化器可能改用 Hash Join,对驱动表大小不敏感(但别依赖,默认仍走 NLJ) - 被驱动表连接字段有唯一索引,而驱动表虽大但过滤后极小(比如加了强
WHERE),此时大表驱动反而更稳 - 多维表 JOIN 事实表时(如 6 个百行维表 + 1 个千万行订单表),优化器搜索空间爆炸,可能误判——这时手动用子查询先聚合小表,再连大表,比硬靠顺序更可靠
真正该盯住的,从来不是“哪个表物理小”,而是 EXPLAIN 里 rows 最小的那个、ON 字段有索引的那个、类型严格匹配的那个。驱动表只是表象,底层成本藏在每次内层查找访问了多少数据页里——这个数字,Handler_read_next 和 Handler_read_key 状态变量比 rows 更真实。



















