子查询慢的首要原因是相关子查询,需用EXPLAIN检查DEPENDENT SUBQUERY标记并为条件字段建联合索引;大结果集时应改写为EXISTS或LEFT JOIN;标量子查询应提前聚合再JOIN;物化提示仅作临时缓解,根本在于索引与过滤优化。

子查询慢,先看是不是相关子查询
相关子查询(correlated subquery)是性能杀手,因为外层每行都会触发一次内层执行。比如 SELECT name FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid') 看似简单,但如果 orders 表没索引、或 user_id 和 status 没联合索引,就会全表扫描多次。
实操建议:
- 用
EXPLAIN看执行计划,重点找DEPENDENT SUBQUERY或MATERIALIZED标记 - 检查内层 WHERE 条件字段是否都有索引,尤其多列组合条件要建联合索引,比如
(user_id, status) - 如果外层结果集大(比如上万行),相关子查询基本不可接受,必须改写
IN/EXISTS 改 JOIN 时注意语义等价性
IN 和 EXISTS 在有 NULL 或重复值时行为不同,直接换成 JOIN 可能多出重复行或漏数据。例如 SELECT * FROM users WHERE id IN (SELECT user_id FROM orders),若 orders.user_id 有重复,JOIN 会放大 users 行数。
实操建议:
- 语义等价优先选
EXISTS→LEFT JOIN ... ON ... WHERE ... IS NOT NULL,更贴近原意 - 想严格去重,用
INNER JOIN ... GROUP BY users.id或加DISTINCT,但注意DISTINCT本身有开销 - 如果子查询里有聚合(如
MAX(created_at)),不能简单 JOIN,得用窗口函数或派生表先算好再关联
WHERE 中的标量子查询(SELECT 单值)怎么安全替换
像 SELECT id, (SELECT COUNT(*) FROM logs WHERE user_id = users.id) AS log_count FROM users 这种,每个用户查一次计数,非常低效。它本质是“为每行计算一个聚合”,不是关联关系。
实操建议:
- 改成先聚合再 JOIN:
SELECT u.id, COALESCE(l.cnt, 0) FROM users u LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM logs GROUP BY user_id) l ON u.id = l.user_id - 别忘了
COALESCE处理无日志用户的 NULL,否则log_count会是 NULL 而非 0 - 如果聚合逻辑复杂(含条件统计、多表关联),把子查询提前作为 CTE 或派生表,避免重复计算
MySQL 8.0+ 和 PostgreSQL 的物化提示可临时缓解,但别依赖
MySQL 8.0 支持 /*+ MATERIALIZE */ 提示强制物化子查询,PostgreSQL 有 MATERIALIZED 关键字。它们能避免反复执行,但只是“绕过问题”,不解决设计缺陷。
实操建议:
- 仅用于紧急上线救火,或临时分析语句;上线前必须回归到 JOIN 改写
- 物化本身占临时内存/磁盘,大数据量下可能触发
Temp table write to disk,反而更慢 - 不同版本优化器对提示的支持不稳定,MySQL 5.7 不识别
MATERIALIZE,PG 12 以下不支持MATERIALIZED
真正卡住的地方,往往在子查询里藏着没走索引的条件,或者外层过滤太宽导致中间结果集爆炸——这些比语法改写更值得花时间盯死。


















