先聚合再join仅适用于维度表极小、数据倾斜严重、聚合逻辑复杂无法主查询表达、或无需明细字段等特定场景;多数情况应优先join后group by。

先聚合再JOIN的适用场景判断
不是所有多表聚合都该先聚合再JOIN——多数时候反而是先JOIN再GROUP BY更高效。只有当你遇到以下情况时,才值得走预聚合路线:
• 维度表极小(比如性别、状态码表),事实表极大(千万级订单)
• 关联字段上存在严重数据倾斜(某user_id占全部订单70%)
• 聚合逻辑复杂且无法在主查询中表达(如带FILTER、窗口函数嵌套、多周期统计)
• 查询只关心聚合结果,不依赖明细字段(比如只要每个城市的总GMV,不要单笔订单号)
子查询预聚合必须写对的三个细节
用子查询实现先聚合再JOIN,最容易因语法或语义错误导致结果错、性能崩、甚至查不出数据:
- 子查询必须有别名(如
AS agg),否则ON里无法引用字段,MySQL报错Every derived table must have its own alias -
GROUP BY字段必须和JOIN条件字段完全一致:类型相同(BIGINT不能对VARCHAR)、名称一致(user_id不能写成uid)、无隐式转换(避免ON u.id = CAST(agg.user_id AS CHAR)) - LEFT JOIN后,聚合字段要用
COALESCE(agg.total_spent, 0),否则NULL会把整行过滤掉,等效于INNER JOIN
CTE比子查询更适合多周期聚合
当你要同时算近7天、30天、90天的订单数,或者按不同状态分组统计,CTE比嵌套子查询更易读、更易维护,执行计划也基本一致:
WITH order_summary AS (
SELECT
user_id,
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days') AS cnt_7d,
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days') AS cnt_30d,
SUM(amount) FILTER (WHERE status = 'paid') AS paid_amount
FROM orders
GROUP BY user_id
)
SELECT
u.name,
COALESCE(os.cnt_7d, 0),
COALESCE(os.cnt_30d, 0),
COALESCE(os.paid_amount, 0)
FROM users u
LEFT JOIN order_summary os ON u.id = os.user_id;
注意:FILTER是PostgreSQL特有语法,MySQL需改用CASE WHEN;CTE本身不物化,只是语法糖,大数据量下仍要靠索引支撑。
索引和统计信息才是预聚合生效的前提
再干净的子查询,如果关联字段没索引或统计信息过期,数据库照样全表扫描——预聚合省下的数据量全被这一步吃掉:
- 确保预聚合子查询里的
WHERE条件字段(如created_at)有索引,且复合索引顺序匹配过滤+分组逻辑,例如(created_at, user_id) - 检查
EXPLAIN输出中是否出现Seq Scan或Using temporary,这是索引未命中或排序开销大的信号 - PostgreSQL/SQL Server定期更新统计信息(
ANALYZE或UPDATE STATISTICS),否则优化器可能误判数据分布,选错执行路径
真正卡住性能的,往往不是“会不会写子查询”,而是“有没有让数据库知道该怎么快速找到那些要聚合的行”。











