JOIN不一定比子查询快,真正决定性能的是执行计划是否触发DEPENDENT SUBQUERY、能否谓词下推及中间结果集是否膨胀;DEPENDENT SUBQUERY意味着外层每行都重执行内层,易导致百万级重复扫描,应优先改写为JOIN或LATERAL。

JOIN 不一定比子查询快,但数据库引擎对 JOIN 的连接转换机制更成熟、更可控;真正决定快慢的,是执行计划里是否触发了 DEPENDENT SUBQUERY、是否能做谓词下推、中间结果集是否膨胀——而不是你写了 JOIN 还是写了 IN。
看到 DEPENDENT SUBQUERY 就该警觉
这是 MySQL/PostgreSQL 执行计划里最典型的性能红灯。它意味着:外层每查一行,内层子查询就得重跑一次。
- 比如
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid'),若users有 100 万行,且orders缺少(user_id, status)复合索引,这个子查询可能被执行 100 万次 -
EXPLAIN输出中若出现DEPENDENT SUBQUERY或select_type=DEPENDENT SUBQUERY,基本等于告诉你要重写成JOIN或改用LATERAL(PostgreSQL)/APPLY(SQL Server) - MySQL 5.6+ 虽支持 semi-join 优化,但仅限于部分
IN和EXISTS场景;相关子查询(correlated subquery)仍大概率逃不过嵌套循环
JOIN 的连接算法选择权在优化器手里
写 JOIN 不等于自动获得哈希连接——优化器是否选对算法,取决于统计信息准不准、驱动表选得对不对、连接字段有没有索引。
- 没索引的
ON字段?大概率退化为嵌套循环,和相关子查询实际执行路径差不多 - 小表没被选为驱动表?
STRAIGHT_JOIN(MySQL)或/*+ leading() */(Oracle/PG)可强制,但需先用EXPLAIN (ANALYZE)确认当前计划 - 大表
LEFT JOIN小表却没加WHERE过滤?中间结果集可能暴涨,内存溢出或落盘临时表,比子查询还慢
子查询不是原罪,但容易踩“重复执行”和“物化陷阱”
标量子查询(scalar subquery)和非相关子查询(non-correlated subquery)在某些场景反而更轻量,但前提是别让它被误判或滥用。
-
SELECT id, name, (SELECT MAX(created_at) FROM logs l WHERE l.user_id = u.id) FROM users u:若logs(user_id, created_at)有索引,这个子查询走索引 MIN/MAX 查找,比LEFT JOIN logs GROUP BY避免了行数膨胀,实际更快 -
WHERE x IN (SELECT y FROM t2 WHERE z = ?):若t2结果集很小(y 有索引,优化器常转为 hash semi-join,性能接近等价JOIN - 但若子查询含
LIMIT、ORDER BY或聚合又没加GROUP BY,MySQL 可能强制物化到磁盘临时表——Using temporary; Using filesort就是信号
真正该盯住的是 EXPLAIN 输出里的三件事
别猜语法快慢,直接看执行计划里这三项是否健康:
-
rows列是否远超实际返回行数?说明统计信息过期,ANALYZE TABLE或VACUUM ANALYZE(PG)要跟上 - 是否有
Using join buffer或Using where; Using index?前者暗示哈希连接启用,后者代表覆盖索引,都是好迹象;而Using temporary或Using filesort往往是子查询物化或 JOIN 后排序的代价 -
Extra里是否出现Not exists(优化过的NOT EXISTS)、FirstMatch(semi-join 优化标志)?有就说明优化器已介入重写,不用硬改
复杂点在于:同一句 SQL,在 MySQL 8.0 和 PostgreSQL 15 下的执行路径可能完全不同;而哪怕同版本,ANALYZE 前后、innodb_stats_persistent 开关状态,都足以让优化器换一条路走。所以别背结论,盯死 EXPLAIN (ANALYZE) 里真实扫描行数和实际耗时。










