LEFT JOIN性能不如INNER JOIN,核心在于其语义强制左表全量扫描且驱动表顺序被锁死,优化器无法跳过左表或交换连接顺序,导致右表索引难生效、I/O开销倍增。

LEFT JOIN 性能不如 INNER JOIN,核心不是语法本身慢,而是驱动表扫描开销被迫放大——左表全量必须参与关联,优化器没法跳过,更难用上右表索引。
LEFT JOIN 的驱动表为什么总是“被锁死”
MySQL/PostgreSQL 的 JOIN 执行顺序受语义约束:LEFT JOIN 的左表(FROM 后第一个表)几乎必然成为驱动表。优化器不能像处理 INNER JOIN 那样自由交换顺序,哪怕右表更小、有更好索引,也得先扫左表每行,再逐条去右表找匹配。
- 写
FROM orders LEFT JOIN customers ON ...→orders就是驱动表,哪怕它有 1000 万行 - INNER JOIN 中,优化器可能选
customers当驱动表(如果它只有 1 万行),大幅减少循环次数 - 驱动表行数越多,右表被访问的次数就越多;若右表没索引,就是 N × M 级 I/O
ON 条件写错,直接让右表索引失效
即使右表字段建了索引,只要 ON 子句里出现隐式转换或函数,索引就退化为全表扫描。LEFT JOIN 下这问题更致命——因为驱动表行数多,扫一次右表的成本被放大了 N 倍。
-
ON t1.id = CAST(t2.ref_id AS CHAR)→t2.ref_id索引无法使用 -
ON t1.code = UPPER(t2.code)→ 函数导致索引失效 -
ON t1.user_id = t2.user_id AND t2.status = 'active'→ 复合条件中t2.status不在索引最左列,Seek 可能变 Scan
WHERE 条件放错位置,LEFT JOIN 白白多扫一遍
这是最隐蔽也最常踩的坑:LEFT JOIN ... WHERE t2.col IS NOT NULL 或 WHERE t2.status = 'done',表面看只是过滤,实际让整个 LEFT JOIN 语义退化为 INNER JOIN,但执行计划仍按左连接逻辑生成——左表全扫、右表全连、最后再筛,三步全做。
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→ 实际等价于INNER JOIN,但多花了左表扫描成本 - 正确写法:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 过滤提前到连接阶段,右表只读匹配行 - 验证方法:EXPLAIN 中看
rows和filtered,若rows接近左表总行数,基本就是被拖累了
统计信息过期会让 LEFT JOIN 更“瞎”
优化器依赖统计信息估算行数,决定是否走 Nested Loops、Hash Match 还是 Merge Join。LEFT JOIN 对估算更敏感——一旦低估右表匹配率,它可能选错算法,比如该用 Index Seek 却用了 Table Scan。
- 执行
ANALYZE TABLE users, orders(MySQL)或VACUUM ANALYZE(PostgreSQL)更新统计 - 特别注意:大表批量导入后没
ANALYZE,LEFT JOIN 很容易选错驱动表或连接方式 - 某些场景下,
optimizer_search_depth设得太低(如 MySQL 默认 6),长 JOIN 链直接放弃重排,硬按书写顺序执行
LEFT JOIN 的性能瓶颈不在“要不要 NULL”,而在“不得不扫”。真正要优化的,从来不是换 JOIN 类型,而是控制驱动集大小、确保右表能 Seek、把过滤逻辑塞进 ON——否则你写的 LEFT JOIN,很可能正在替 INNER JOIN 承担双倍开销。


















