JOIN 与 IN/EXISTS 无绝对快慢,关键看执行计划是否触发 DEPENDENT SUBQUERY、中间结果集是否膨胀及连接字段有无索引;出现 DEPENDENT SUBQUERY 即性能失控,需改写为半连接或确保索引覆盖。

JOIN 和 IN / EXISTS 子查询没有绝对快慢,真正起决定作用的是执行计划是否触发 DEPENDENT SUBQUERY、中间结果集是否膨胀、连接字段有没有索引。
看到 DEPENDENT SUBQUERY 就该立刻警惕
这是 MySQL / PostgreSQL 执行计划里最危险的信号。它意味着外层每查一行,内层子查询就得重跑一次。
- 比如
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id),如果users有 50 万行,orders缺少(user_id)索引,实际就是 50 万次单行扫描 -
EXPLAIN输出中只要出现select_type = DEPENDENT SUBQUERY,基本等于性能已失控,必须改写 - 这种场景下,
LEFT JOIN或EXISTS改写为半连接(semi-join)通常更稳——但前提是优化器能识别且没被禁用
IN 子查询在 MySQL 5.6+ 常被自动转成 semi-join
只要你写的 IN 子查询满足几个简单条件:不带 GROUP BY、不带 LIMIT、不带 ORDER BY、子查询字段有索引,MySQL 优化器大概率把它重写成和 JOIN 几乎一样的执行路径。
-
EXPLAIN里可能看到type: eq_ref或Using join buffer,说明已走半连接,性能与等价JOIN拉不开差距 - 但如果子查询里写了
SELECT DISTINCT user_id FROM orders GROUP BY user_id,优化器直接放弃重写,退化为逐行物化,性能断崖下跌 -
NOT IN几乎从不被转成 semi-join,尤其当右表含NULL时还会逻辑错误,一律优先换NOT EXISTS
中间结果集大小比语法形式更致命
一个 LEFT JOIN 把 1 万用户 × 平均 200 订单 = 200 万行中间结果,后续再 GROUP BY 或 ORDER BY,内存和排序压力远超一个只返回布尔值的 EXISTS。
- 标量子查询(如
SELECT name, (SELECT MAX(created_at) FROM logs l WHERE l.user_id = u.id))若logs(user_id, created_at)有复合索引,实际走索引 MIN/MAX 查找,比JOIN + GROUP BY更轻量 -
DISTINCT在JOIN后补救重复行,常引发Using temporary和Using filesort,不如一开始就用EXISTS表达存在性语义 - 多对多关联未加限制(如学生×课程×成绩三表连查)极易膨胀,此时先用子查询物化中间结果(
WITH或临时表)反而可控
索引失效会让所有写法都变慢
无论你写 JOIN 还是 IN,只要 ON 或 WHERE 条件字段没走索引,全表扫描开销就盖过一切语法差异。
-
INNER JOIN t1 ON t1.a = t2.a中,t2.a没索引 → 退化为嵌套循环,和相关子查询实际执行路径差不多 - 复合索引顺序错位(如查询
WHERE user_id = ? AND created_at > ?却建了(created_at, user_id))→ 索引完全失效 -
EXISTS内层依赖外层字段时,那个字段必须有索引;否则仍是全表扫,EXISTS的“短路”优势归零
真正容易被忽略的是 rows 字段和 Extra 里的线索:Using temporary、Using filesort、Rows_examined 远超预期,这些才是性能瓶颈的实锤——而不是纠结该写 JOIN 还是 IN。


















