相关子查询聚合慢因每次外层行都重执行内层查询,致O(n×m)复杂度;改用JOIN+GROUP BY可降为O(n+m),并需添加(user_id, status)复合索引优化。

为什么相关子查询聚合会慢
相关子查询(比如 SELECT ... (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) FROM users u)每次外层行都要重新执行内层查询,数据量一上去就变成 O(n×m) 复杂度。数据库没法提前物化中间结果,索引也常被绕过,尤其当内层有 WHERE 条件依赖外层字段时,优化器往往放弃哈希/排序合并连接,只能嵌套循环。
常见症状包括:执行计划里反复出现 DEPENDENT SUBQUERY、Using temporary; Using filesort,且 EXPLAIN 显示 rows 列乘积远超总行数。
用 JOIN + GROUP BY 替代是最直接的改法 把相关子查询“拉平”,让聚合在连接后一次性算完,数据库能走 Hash Join 或索引扫描,复杂度降到 O(n+m)。
改写要点:
- 把子查询的
FROM表提到主查询,和主表JOIN - 用
GROUP BY对主表关键字段(如user_id)分组 - 聚合函数(
COUNT()、SUM()、AVG())放SELECT里,不带WHERE条件 - 原相关条件(如
o.user_id = u.id)转为ON子句 - 如果需要保留主表无匹配的行(比如用户没订单也要显示 0),用
LEFT JOIN,再用COUNT(o.id)(不是COUNT(*))
示例:
-- 原慢写法 SELECT u.name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id AND o.status = 'paid') cnt FROM users u; <p>-- 改写后 SELECT u.name, COUNT(o.id) cnt FROM users u LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid' GROUP BY u.id, u.name;
注意 NULL 和去重逻辑不能丢COUNT(*) 在 LEFT JOIN 后会把主表空匹配行计为 1,必须用 COUNT(非空列)(如 o.id)才能得 0;若原子查询含 DISTINCT(如 COUNT(DISTINCT o.product_id)),改写后 GROUP BY 范围必须包含所有 SELECT 非聚合字段,否则 MySQL 会报错或结果错乱。
容易踩的坑:
- 漏写
GROUP BY所有非聚合字段,导致 MySQL 5.7+ 模式下报错,或低版本返回随机值 - 用
COUNT(*)代替COUNT(o.id),把无订单用户计成 1 而非 0 - 把过滤条件写在
WHERE而非ON,把LEFT JOIN变成INNER JOIN,丢失空匹配行 - 聚合字段有
NULL,又没加COALESCE(),前端拿到NULL而非 0
大表关联时加索引比改写更关键
即使改成了 JOIN + GROUP BY,如果 orders 表上没有 (user_id, status) 复合索引,还是得全表扫描。优先建索引:
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
注意顺序:user_id 必须在前,因为它是连接键;status 在后用于过滤。如果查询还涉及时间范围(如 created_at > '2024-01-01'),索引要扩展为 (user_id, status, created_at)。
改写本身不难,但索引缺失会让效果打折扣——很多人调完 SQL 就以为完事了,其实执行计划里还卡在 type: ALL 上。

















