“先聚合再join”能显著提速,因其避免中间结果集膨胀、降低join开销、提升索引命中率;mysql用派生表,postgresql/mysql8.0+推荐cte实现。

“先聚合再JOIN”不是银弹,但对带WHERE过滤的多表GROUP BY查询,它往往是性能翻倍最直接的解法。
为什么GROUP BY + JOIN会慢得离谱
典型现象是:EXPLAIN里rows字段突然跳到几十万甚至上百万,Extra列出现Using temporary或Using filesort。这不是因为SUM()或COUNT()本身慢,而是JOIN先把orders、logs、events这些事实表“撑爆”了,再分组——相当于先搬一吨砖上楼,再挑出其中几块来称重。
真正瓶颈在中间结果集膨胀。比如查“每个用户近30天订单总金额+等级”,原始写法让orders和users先JOIN,哪怕加了WHERE created_at >= '2026-03-22',JOIN后仍要处理数十万行;而如果先按user_id聚合orders,结果只剩几千行,再JOINusers,驱动表扫描量直接下降两个数量级。
MySQL 5.7+ 怎么写“先聚合再JOIN”
必须用派生表(subquery in FROM),不能依赖优化器自动重写。注意别名、NULL处理和LEFT JOIN语义保持。
- 聚合子查询里只保留
GROUP BY字段和聚合结果,不要带无关字段 - 外层
LEFT JOIN时,右表字段可能为NULL,记得用COALESCE(o.sum_amount, 0)兜底 - 子查询的
WHERE条件必须写在内部,不能挪到外层,否则失去提前过滤意义 - 确保
orders(user_id, created_at, amount)有复合索引,否则子查询本身就会全表扫
示例:
SELECT u.name, u.level, COALESCE(o.sum_amount, 0) AS total FROM users u LEFT JOIN ( SELECT user_id, SUM(amount) AS sum_amount FROM orders WHERE created_at >= '2026-03-22' GROUP BY user_id ) o ON u.id = o.user_id;
PostgreSQL / MySQL 8.0+ 推荐用CTE
语义更清晰,维护性更好,且优化器通常能更好下推谓词。但要注意:CTE在PostgreSQL中默认不物化(除非加MATERIALIZED),在MySQL 8.0+中则是可选物化,行为一致。
- CTE名需唯一,避免和真实表名冲突
- 若需多次引用同一聚合结果,CTE比重复写派生表更安全
- 不要在CTE里写
ORDER BY或LIMIT,除非明确需要截断——这会干扰优化器选择执行路径 - 多维聚合(如按
user_id, category_id)也应放在CTE内完成,外层只负责补维度名称
示例:
WITH order_summary AS ( SELECT user_id, category_id, SUM(amount) AS sum_amount FROM orders WHERE created_at >= '2026-03-22' GROUP BY user_id, category_id ) SELECT u.name, c.name AS category, COALESCE(o.sum_amount, 0) FROM users u LEFT JOIN order_summary o ON u.id = o.user_id LEFT JOIN categories c ON o.category_id = c.id;
什么情况下不该强行“先聚合再JOIN”
盲目拆解反而引入额外IO或语义错误。常见陷阱包括:
- 聚合字段依赖右表内容(如
GROUP BY u.level, o.status),此时提前聚合会丢失o.status上下文 - 右表数据量极小(几百行),且JOIN后过滤条件很强,强行拆可能增加一次全表扫描
- 使用了窗口函数或需要逐行计算的逻辑,聚合后无法还原原始粒度
- 子查询结果集仍很大(比如按小时统计日志,返回上万行),JOIN开销并未显著降低
最稳妥的做法:先跑EXPLAIN ANALYZE看原始SQL的rows和实际耗时,再写聚合子查询版本对比——差10倍以上才值得投入。别为省10ms去动一个稳定运行三年的SQL。










