子查询慢八成是执行计划失控,非写法问题:explain显示dependent subquery即表明每行外层都重跑子查询,rows远超返回行数且extra含using temporary/using filesort,坐实嵌套循环放大与索引失效。

子查询慢,八成不是写法问题,而是执行计划已经失控——EXPLAIN里一看到 DEPENDENT SUBQUERY 或 type=ALL 且 rows 远超返回行数,基本可以断定是嵌套循环放大在作祟。
怎么看子查询是不是真瓶颈?
别猜,直接看执行计划和慢日志里的原始指标:
-
EXPLAIN中type列为DEPENDENT SUBQUERY:说明子查询每行外层都重跑一次,10万行主表 = 10万次子查询执行 -
rows值异常高(比如扫描 500 万行,只返回 200 行),配合Extra出现Using temporary; Using filesort,基本坐实索引失效 + 临时表拖垮性能 - 慢日志中
Rows_examined和Rows_sent比值 > 1000:说明大量扫描做了无用功,极大概率是子查询 WHERE 条件没走索引或用了函数 - SQL Server 用户注意:
sys.dm_exec_query_stats查total_elapsed_time高的query_hash,再用sys.dm_exec_sql_text定位具体 SQL,比肉眼扫日志快得多
为什么加了索引子查询还是慢?
索引建了 ≠ 能用上。子查询里字段顺序、类型、表达式稍有偏差,索引就彻底失效:
- 子查询写的是
WHERE user_id = ? AND status = 'error',但只建了单列user_id索引——必须补上联合索引(user_id, status) - WHERE 里用了函数,比如
DATE(create_time) = '2026-07-20',哪怕create_time有索引也白搭 - 字段类型不一致:子查询中
user_id是VARCHAR,主表关联字段却是INT,隐式转换导致索引失效 - 子查询带
ORDER BY + LIMIT 1(如取最新订单),MySQL 会拒绝物化,强制每次排序——这种不能靠加索引解决,得改结构
IN/EXISTS 子查询改 JOIN 时最容易翻车的点
不是所有 IN 都能无脑换 JOIN,漏掉任一条件就会结果错或更慢:
-
NOT IN子查询结果含NULL?直接换LEFT JOIN ... IS NULL会漏数据,必须加AND right.id IS NOT NULL显式排除 - 主表和子表是一对多(如用户→订单),
INNER JOIN后没加DISTINCT或GROUP BY,COUNT/SUM 全乱套,但加了又可能引入额外排序开销 - 子查询里有聚合(
COUNT(*))或GROUP BY,硬 JOIN 会导致笛卡尔积爆炸,必须先聚合再LEFT JOIN - JOIN 字段没索引?那比原 IN 还慢——
EXPLAIN里key=NULL就是铁证
标量子查询(SELECT 单值)怎么避免逐行触发?
像 SELECT id, (SELECT COUNT(*) FROM logs WHERE user_id = u.id) AS cnt FROM users u 这种,本质是“每行查一次聚合”,开销随主表行数线性增长:
- 正确做法:把子查询提前算好,
SELECT user_id, COUNT(*) AS cnt FROM logs GROUP BY user_id,再LEFT JOIN回主表 - 如果日志表太大(亿级),聚合前务必加时间过滤,比如
WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY) - 别在子查询里引用主表字段做条件(如
WHERE user_id = u.id AND status = u.status)——这又变回相关子查询,前功尽弃 - MySQL 8.0+ 可尝试加
/*+ MATERIALIZE */提示,但老版本无效,且不如显式提前聚合可靠
真正难的不是怎么改写,而是判断哪一层括号在执行时会触发重复计算——很多 SQL 看似只多了一层 SELECT,实则让扫描行数从 10 万跳到 1000 万。改完之后,务必用 EXPLAIN 对比 rows 和 Extra,而不是只看执行时间;缓存会让时间失真,但扫描行数不会说谎。











