LEFT JOIN 比 INNER JOIN 更易拖慢查询,因其语义强制保留左表所有行,优化器无法自由交换驱动表或提前裁剪;右表无索引时触发全表扫描,且 ON 条件写错、类型不匹配或统计信息陈旧会进一步放大性能问题。

LEFT JOIN 为什么比 INNER JOIN 更容易拖慢查询
因为 LEFT JOIN 的语义约束让优化器“不敢乱动”——它必须保留左表所有行,不能像 INNER JOIN 那样自由交换驱动表顺序或提前裁剪。一旦你写了 FROM a LEFT JOIN b ON ... LEFT JOIN c ON ...,MySQL(尤其 5.7/8.0 默认配置)大概率就按这个顺序执行,哪怕 b 是千万级宽表、c 没索引,它也得硬扫。
常见现象:加了 EXPLAIN 发现 type 是 ALL 或 index,rows 高达百万,但你明明在 WHERE 里写了过滤条件——问题往往出在右表没被提前过滤,或者驱动表选错了。
- INNER JOIN 可以由优化器自动选小表当驱动表,LEFT JOIN 的驱动表基本就是
FROM后那个 - LEFT JOIN 右表每行匹配失败,都要补一整行
NULL,内存和网络开销翻倍 - 如果右表有
OR、函数、隐式转换(比如ON a.id = CAST(b.ref_id AS CHAR)),索引直接失效
JOIN 链越长,优化器越容易“放弃治疗”
三张表以上 LEFT JOIN 连写时,优化器的搜索空间指数级增长,而 MySQL 默认 optimizer_search_depth=6,PostgreSQL 的 join_collapse_limit 默认是 8,超过就退化为固定顺序执行。这不是 bug,是权衡:保证语义正确比猜最优路径更重要。
典型症状:EXPLAIN FORMAT=TREE 显示嵌套很深,最内层是 table_b 全表扫描;或者 EXPLAIN ANALYZE 里某一步耗时占整体 90% 以上,且 Buffers 读取量巨大。
- 避免写
FROM a LEFT JOIN b ON ... LEFT JOIN c ON ... LEFT JOIN d ON ...这种长链,优先拆成子查询或 CTE - MySQL 8.0+ 可用
/*+ SET_VAR(optimizer_search_depth = 8) */临时调高,但不解决根本问题 - PostgreSQL 中
SET join_collapse_limit = 1强制禁用重排,反而能让执行计划更可预测(适合已知最优顺序的场景)
WHERE 条件写在 ON 还是 WHERE,性能差十倍
LEFT JOIN 后跟 WHERE b.status = 'done' 看似合理,实则让整个 LEFT JOIN 退化为 INNER JOIN 效果——因为 WHERE 是在连接完成后再过滤,所有 b.status IS NULL 的行全被干掉。优化器可能因此放弃使用索引,转而先连再筛。
正确做法是把右表的过滤条件塞进 ON,或用子查询提前收缩右表:
- ✅
LEFT JOIN (SELECT * FROM orders WHERE status = 'done') o ON u.id = o.user_id - ✅
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'done' - ❌
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'done'(等价于 INNER JOIN)
最容易被忽略的三个性能断点
不是没建索引,而是索引没用上;不是 SQL 写错,而是执行路径被隐式规则锁死。
-
ON字段类型不一致:比如a.id是BIGINT,b.a_id是VARCHAR,触发隐式转换,索引失效 - 复合索引没对齐最左前缀:
ON b.a_id = a.id AND b.deleted = 0,但索引是(deleted, a_id),而非(a_id, deleted) - 统计信息陈旧:
ANALYZE TABLE没跑过,优化器误判右表只有 100 行,实际是 200 万行,于是选错驱动表
真正卡住性能的,往往不是 JOIN 类型本身,而是驱动顺序、索引覆盖、条件位置这三处细节。查慢查询时,别急着改 SQL 结构,先看 EXPLAIN 输出里哪一行 rows 最大、type 最差、key 是不是 NULL。


















