子查询慢的本质是被反复执行,尤其出现dependent subquery时,外层每行触发内层一次执行;即使有索引,10万行仍导致10万次独立查找;in/exists不会自动优化,语义不等价则不重写;标量子查询最隐蔽,应改用join或派生表。

子查询本身不慢,慢的是它被反复执行——尤其是当你看到 DEPENDENT SUBQUERY 出现在 EXPLAIN FORMAT=TREE 里时,基本可以判定:外层每扫一行,内层就重跑一遍。
为什么 EXPLAIN 显示 DEPENDENT SUBQUERY 就一定慢?
只要子查询里引用了外层表的列(比如 t1.id = t2.user_id),它就是相关子查询。MySQL 无法提前算出结果,只能逐行触发执行。
- users 表 10 万行 → orders 子查询执行 10 万次
- 哪怕
orders.user_id有索引,每次仍是独立索引查找 + 聚簇回表 - 若子查询含
GROUP BY、ORDER BY RAND()或窗口函数,连物化机会都没有,必然退化为嵌套循环
IN / EXISTS 真能自动优化?别信
很多人把 IN 换成 EXISTS 就以为能触发物化或半连接,其实不会。两者语义不同,优化器只在严格等价时才重写。
-
EXISTS返回布尔值,IN返回值列表,语义不等价 → 不重写 - 子查询里有
t1.id = t2.parent_id?无论写IN还是EXISTS,都是DEPENDENT SUBQUERY -
EXISTS (SELECT 1 FROM ...)比SELECT *更安全,但前提是关联字段不能为NULL,且WHERE条件字段必须有索引
标量子查询(SELECT 列表里的子查询)最隐蔽的坑
这种写法看着干净:SELECT u.id, (SELECT MAX(order_time) FROM orders o WHERE o.user_id = u.id) FROM users u,但它本质是“快递员每送一个包裹,就跑一趟仓库查单号”。
- users 表 10 万行 →
MAX(order_time)执行 10 万次 - 即使
orders.user_id有索引,累计开销也远超一次聚合预计算 - 优化器极少自动上拉(pull-up)这类子查询,除非满足极严格的条件(无聚合、无排序、无外部引用)
真正可控的替代方案只有 JOIN 和派生表
JOIN 的执行路径更透明:驱动表可选、索引可复用、MySQL 8.0+ 支持 hash join;而子查询的物化与否全靠隐式判断,稍有偏差就崩。
-
LEFT JOIN ... ON ... IS NULL替代NOT IN,避开临时表和NULL陷阱 -
INNER JOIN替代IN,让优化器有机会用semijoin,比物化快得多 - 派生表要确保不引用外层列,例如
(SELECT DISTINCT user_id FROM logins WHERE ...)写成独立子句再JOIN
最容易被忽略的,是物化到底有没有发生——不能只看 MySQL 版本或 optimizer_switch 设置,必须用 EXPLAIN FORMAT=TREE 或 optimizer_trace 看清优化器实际选了哪条路。一旦看到 Using temporary; Using filesort,说明物化失败,你正在跑 10 万次嵌套循环。










