直接看执行计划中嵌套层级最深、rows预估最大、cost占比最高的操作节点,它大概率就是瓶颈子查询;SQL Server用“显示实际执行计划”定位独立算子,MySQL用EXPLAIN FORMAT=JSON查看subquery字段的rows和filtered值。

怎么看执行计划里哪个子查询拖慢了整条 SQL
直接看执行计划中嵌套层级最深、rows预估最大、cost占比最高的那个操作节点,它大概率就是瓶颈子查询。SQL Server 的实际执行计划(“显示实际执行计划”)里,每个子查询会以独立的“Nested Loops”或“Compute Scalar”等算子出现;MySQL 的 EXPLAIN FORMAT=JSON 会把子查询展开为 subquery 字段,里面包含完整的 rows 和 filtered 值。
重点盯这几个字段:
-
type是ALL或index:说明子查询在做全表扫描或索引全扫,没走有效过滤条件 -
key为NULL:子查询 WHERE 条件里的字段没命中索引,常见于用了函数(如YEAR(order_date))、类型隐式转换(varchar字段和int值比较) -
rows明显高于主查询表行数:比如主表users有 1 万行,子查询SELECT id FROM orders WHERE user_id = ?却预估扫描 50 万行,说明orders.user_id缺少索引或统计信息过期
为什么用 IN 跟 EXISTS 性能差很多
不是语法本身慢,而是优化器对两种写法的执行策略完全不同:IN 子查询结果集会被物化成临时结构,再逐行匹配;EXISTS 则是对外层每行做一次半连接探测,一旦找到就短路退出。当子查询结果集大、外层数据少时,EXISTS 更优;反过来,子查询结果集小(比如几十行)、外层表大,IN 可能更快。
但真实场景下容易踩的坑:
-
IN (SELECT ...)中子查询返回NULL:整个条件恒为UNKNOWN,导致无结果——这不是性能问题,但会让排查者误以为“查不出来”,其实是逻辑错误 -
IN子查询里用了ORDER BY或LIMIT:MySQL 8.0+ 会拒绝执行,报错ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' -
EXISTS写法中关联字段类型不一致:比如外层users.id是BIGINT,子查询orders.user_id是VARCHAR,会导致索引失效,key显示为NULL
子查询改写成 JOIN 后反而更慢?检查这三处
JOIN 不一定比子查询快,尤其当改写后引入了重复数据、丢失了原语义的短路逻辑,或者让优化器选错了驱动表顺序时。
典型反模式:
- 把
WHERE x IN (SELECT y FROM t2 WHERE ...)改成JOIN t2 ON t1.x = t2.y,但t2.y不唯一 → 主表记录被放大,rows暴涨几倍甚至几十倍 - 改写后没加
DISTINCT或GROUP BY,导致结果集膨胀,后续ORDER BY或聚合成本飙升 -
JOIN的驱动表选反了:比如把大表orders放左边、小表users放右边,优化器被迫对大表做多次循环查找
验证方法:对改写后的 SQL 执行 EXPLAIN,对比 rows 和 Extra 里的 Using temporary、Using filesort 是否新增 —— 有就是退化信号。
统计信息不准会让子查询“看起来快,实际慢”
优化器依赖表的行数、列值分布等统计信息来估算子查询代价。如果长期没更新,它可能认为子查询只查 100 行,实际要扫 10 万行,于是选了嵌套循环(Nested Loops),而本该选哈希连接(Hash Join)。
手动刷新方式:
- MySQL:
ANALYZE TABLE orders;(立即生效,无需重启) - SQL Server:
UPDATE STATISTICS orders WITH FULLSCAN;(FULLSCAN比默认采样更准,适合数据倾斜严重时) - PostgreSQL:
ANALYZE orders;
注意:不要在高峰期频繁执行 FULLSCAN,它会锁表并消耗 IO;日常用默认采样即可,只在发现执行计划突变、rows 预估严重偏离实际时才用全量。
子查询性能问题最麻烦的地方在于:它常藏在看似简单的语句里,而执行计划又不会直接标出“这是第几个子查询”。必须结合 id 和 select_type 字段人工回溯嵌套关系,稍不注意就会看串行 —— 尤其是多层相关子查询嵌套时,EXPLAIN FORMAT=JSON 是唯一靠谱的定位方式。



















