left join多个子查询性能更差,因mysql 8.0+优化器对嵌套子查询重写能力有限,若子查询未显式过滤、无索引支撑或含limit/having,易退化为先展开再关联,导致中间结果集爆炸、内存激增、i/o飙升。

LEFT JOIN多个子查询时,为什么性能反而更差
直接把多个表写成子查询再LEFT JOIN,不等于自动优化。MySQL 8.0+ 的优化器对嵌套子查询的重写能力有限,尤其当子查询里没显式限制数据量、没索引支撑、或含 LIMIT / HAVING 等非标准过滤时,容易退化为“先展开再关联”,中间结果集爆炸。比如:SELECT * FROM a LEFT JOIN (SELECT * FROM b) b1 ON ... LEFT JOIN (SELECT * FROM c) c1 ON ...,若 b 和 c 各有 10 万行,且无有效过滤,JOIN 前就已加载全部数据进内存,后续关联开销远超直接 JOIN 原表。
必须提前在子查询里做三件事
子查询不是“隔离性能”的魔法盒,它只是逻辑封装层。真正起作用的是子查询内部是否做了收缩:
- 显式加
WHERE过滤主键或高选择性字段(如status IN ('shipped', 'delivered')),不能依赖外层WHERE下推 - 确保子查询中
ON字段有索引——不是原表有就行,子查询结果集若被物化(derived表),MySQL 无法复用原表索引,得靠子查询本身输出列可走索引(例如 SELECT 中包含索引覆盖字段) - 禁用
SELECT *,只选真正需要的列;否则即使子查询加了 WHERE,也会因回表导致大量 I/O
用 HAVING 强制物化子查询能提速,但有陷阱
实测中,在子查询末尾加一个无实际过滤效果的 HAVING 1 = 1 或 HAVING sort IS NOT NULL,有时能让 MySQL 把子查询结果提前物化为临时表,并启用更优的 JOIN 策略。这不是文档行为,而是优化器对“不可下推条件”的响应机制。但要注意:
-
HAVING必须作用于子查询中已计算出的字段(如聚合结果、别名),不能引用外层字段 - MySQL 5.7 不支持该技巧,仅 8.0+ 在部分场景生效;PostgreSQL 则需用
MATERIALIZED显式提示 - 如果子查询本身已很小(HAVING 可能引入额外解析开销,反而变慢
替代方案:用 LATERAL(MySQL 8.0.14+)或相关子查询更可控
当多个子查询都依赖左表某字段(如每条订单查最新物流状态、最新评价),LATERAL 比并列子查询更安全:
SELECT o.id, l.tracking_no, r.score FROM orders o LEFT JOIN LATERAL ( SELECT tracking_no FROM logistics WHERE order_id = o.id ORDER BY update_time DESC LIMIT 1 ) l ON TRUE LEFT JOIN LATERAL ( SELECT score FROM reviews WHERE order_id = o.id ORDER BY create_time DESC LIMIT 1 ) r ON TRUE;
这种写法天然避免中间结果膨胀,每行只执行一次子查询,且能利用 order_id 索引快速定位。但注意:LATERAL 子查询不能引用其他 LATERAL 的输出列,顺序敏感;且 MySQL 对 LATERAL 的索引下推支持仍弱于 PostgreSQL。
最易被忽略的一点:子查询的性能不取决于“写了几个”,而取决于“每个子查询实际扫描了多少行”。哪怕只写一个子查询,若没 WHERE、没索引、返回全表,它就会拖垮整个 LEFT JOIN 链。优化永远从数据量控制开始,而不是语法包装。











