子查询慢需先查执行计划,MySQL出现DEPENDENT SUBQUERY或PostgreSQL类似Nested Loop+Materialize即性能隐患;优先改JOIN,注意IN/EXISTS/JOIN语义差异及NOT IN的NULL陷阱。

子查询慢到卡住,先看执行计划里有没有 DEPENDENT SUBQUERY
MySQL 里一旦出现 DEPENDENT SUBQUERY,基本可以断定性能已经掉坑里了——它意味着外层每查一行,内层子查询就得重新执行一次,数据量一上万,响应时间就指数级上涨。PostgreSQL 虽不标这个字样,但类似写法(比如 WHERE x IN (SELECT ...) 套在大表上)也会触发 Nested Loop + Materialize,效果差不多。
实操建议:
- 用
EXPLAIN FORMAT=TREE(MySQL 8.0+)或EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL)确认子查询是否被优化器“展开”;没展开就是隐患 - 把
WHERE col IN (SELECT col FROM t2)改成INNER JOIN t2 ON t1.col = t2.col,尤其当t2.col有索引时,效率通常提升 5–50 倍 - 如果子查询带
GROUP BY或聚合(如SELECT id, (SELECT COUNT(*) FROM logs l WHERE l.user_id = u.id) cnt FROM users u),优先改写为LEFT JOIN ... GROUP BY,避免重复扫描
JOIN 改写时,IN、EXISTS、JOIN 的语义差异不能硬套
不是所有子查询都能无脑换成 JOIN。比如 WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid') 和 WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id AND status = 'paid') 行为一致,但和 INNER JOIN orders ON users.id = orders.user_id WHERE orders.status = 'paid' 在结果集上可能不同:前者去重,后者的 JOIN 可能因一对多产生重复行。
实操建议:
-
IN子查询等价于JOIN+DISTINCT或GROUP BY,若原逻辑允许重复,可直接JOIN;否则加DISTINCT或用EXISTS更安全 -
NOT IN有 NULL 致命陷阱(只要子查询返回任意NULL,整条结果为空),必须改成NOT EXISTS或LEFT JOIN ... WHERE t2.id IS NULL - PostgreSQL 中
IN对小结果集(JOIN 快,因为走 Hash Semi Join;但 MySQL 5.7/8.0 对IN优化较弱,一律优先测JOIN
改写后变慢?检查 JOIN 字段的索引和数据类型是否严格匹配
常见现象:明明把子查询换成了 JOIN,执行时间反而更长。大概率是 ON 条件字段没索引,或者两边类型不一致(比如一边是 VARCHAR(32),另一边是 CHAR(32),或隐式转换导致索引失效)。
实操建议:
- 用
SHOW INDEX FROM table_name(MySQL)或\d table_name(PostgreSQL)确认JOIN字段是否有可用索引;复合索引要注意最左前缀 - 对比
EXPLAIN输出中的key和rows:如果key是NULL或rows高得离谱,说明索引没用上 - 检查字段类型:用
DESCRIBE table看两边是否完全一致;特别警惕INTvsBIGINT、VARCHARvsTEXT、带/不带COLLATE的字符串字段
复杂子查询嵌套三层以上?拆成临时表或 CTE 分步固化中间结果
当子查询本身已含 GROUP BY、窗口函数或多表关联,强行塞进单条 JOIN 会让可读性归零,优化器也容易选错执行路径。这时候硬改不如分治。
实操建议:
- MySQL 5.7 不支持 CTE,可用
CREATE TEMPORARY TABLE tmp AS SELECT ...先存中间结果,再与主表JOIN;记得加索引:ALTER TABLE tmp ADD INDEX idx_user_id (user_id) - PostgreSQL / MySQL 8.0+ 推荐用
WITH filtered_orders AS (SELECT user_id, COUNT(*) c FROM orders WHERE status='paid' GROUP BY user_id) SELECT * FROM users u JOIN filtered_orders o ON u.id = o.user_id - 临时表或 CTE 的关键优势:让优化器对中间结果做准确行数估算,避免“猜错基数→选错连接算法→全表扫”死循环
改写子查询不是机械替换,真正卡点往往藏在索引缺失、类型隐式转换、或中间结果集膨胀这些细节里。跑一遍 EXPLAIN 比读十遍文档都管用。











