聚合子查询慢的本质是每行主表触发一次全量聚合扫描,正确做法是先独立聚合再LEFT JOIN,需确保索引、过滤和执行计划优化。

聚合子查询慢,几乎总是因为每行主表都触发一次全量聚合扫描——10万用户,就执行10万次 COUNT(*) 或 SUM(),不是写法错,是执行模型崩了。
看 EXPLAIN 确认是否掉进相关子查询陷阱
执行 EXPLAIN 后,重点盯两个字段:
-
type列为DEPENDENT SUBQUERY:说明子查询依赖外层字段(如WHERE user_id = u.id),无法物化,必慢 -
rows列数值远超结果行数(比如显示扫描 50 万行,只返回 300 行):典型嵌套循环放大 -
Extra出现Using temporary; Using filesort:没走索引,还建了临时表
标量子查询必须提前聚合,不能留在 SELECT 里
像这种写法:
SELECT u.id, u.name, (SELECT COUNT(*) FROM logs l WHERE l.user_id = u.id) AS log_count FROM users u
本质是“每行 u 触发一次全表 logs 扫描”。正确做法是把聚合逻辑前置:
- 先独立算好聚合结果:
SELECT user_id, COUNT(*) AS cnt FROM logs GROUP BY user_id - 再用
LEFT JOIN关联回主表,避免重复计算 - 如果
logs表极大(如上亿行),务必加时间过滤:WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY) - 注意:
GROUP BY字段必须和JOIN条件完全一致,否则关联失效
IN/EXISTS 类聚合子查询改 JOIN 的硬约束
不是所有带聚合的子查询都能直接 JOIN,漏掉任一条件就会出错或更慢:
- 主表和子表是一对多关系(如一个用户有 N 条日志),直接
INNER JOIN会导致主表行数膨胀——必须加DISTINCT或改用EXISTS - 子查询含
ORDER BY + LIMIT 1(如取每个用户的最新订单),不能直连,得先用派生表或窗口函数预聚合:SELECT user_id, MAX(create_time) FROM orders GROUP BY user_id - 子查询结果可能含
NULL,而NOT IN遇NULL恒为FALSE——此时必须用NOT EXISTS或显式过滤IS NOT NULL
容易被忽略的关键点
很多人改完 SQL 就以为结束了,但真正决定快慢的是三件事:
-
JOIN字段有没有索引?users.id和logs.user_id都得有索引,联合索引更佳 - 聚合前是否加了有效过滤?没加
WHERE status = 'active'或时间范围,等于白优化 - 改写后是否对比了
EXPLAIN的rows和Extra?执行时间受缓存干扰,扫描行数不会骗人










