必须先对一对多表按业务主键group by聚合再left join;否则join导致行数膨胀,sum/count结果错误。例如1用户3订单2标签→6行,聚合值被重复计算,且膨胀不可逆。

直接回答:必须把每张一对多表,各自按业务主键(如 user_id、order_id)先 GROUP BY 聚合成一行,再用 LEFT JOIN 对齐主表;否则 SUM、COUNT 算出来就是错的,不是慢,是不可信。
为什么JOIN后SUM/COUNT一定翻倍
数据库只做行拼接,不做自动去重。一个用户有 3 笔订单、2 个标签,LEFT JOIN orders + LEFT JOIN user_tags 后,该用户数据会膨胀成 3 × 2 = 6 行。后续所有聚合都跑在这 6 行上:COUNT(*) 变成 6,SUM(order_amount) 被重复累加,结果可能是真实值的 2–5 倍。
常见错误现象:
-
EXPLAIN显示某步rows突增 10 倍以上 - 加了
GROUP BY user_id也救不回来——膨胀已固化在 JOIN 结果里 -
COUNT(DISTINCT order_id)看着正常,但金额类指标完全无法补救
子查询预聚合的标准写法
核心是让“多”侧表在 JOIN 前就压缩成单行,且聚合字段必须和外层 ON 条件严格对齐。
正确写法要点:
- 子查询必须有别名,例如
AS o_agg,否则ON无法引用 -
GROUP BY字段必须和子查询中非聚合字段完全一致(MySQL 8.0+ / PostgreSQL 会报错) - 过滤条件(如
WHERE status = 'paid')必须写在子查询内部,不能挪到外层WHERE,否则LEFT JOIN变成INNER JOIN - 用
COALESCE(o_agg.total_amount, 0)显式补零,避免 NULL 导致前端崩溃或报表归零
示例(统计每个用户最近 30 天订单数 + 总金额):
SELECT
u.id,
u.name,
COALESCE(o_agg.order_count, 0) AS order_count,
COALESCE(o_agg.total_amount, 0) AS total_amount
FROM users u
LEFT JOIN (
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY user_id
) AS o_agg ON u.id = o_agg.user_id;
LEFT JOIN 中 ON 和 WHERE 的位置决定是否丢数据
这不是语义习惯问题,是物理执行顺序问题:
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→ 先全量 JOIN 出所有订单行,再过滤,膨胀已发生,且没订单的用户直接被踢掉 - 正确写法:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 右表只拉status = 'paid'的行进来,从源头控量,保留无订单用户
如果右表本身存在重复(比如历史订单表含多条同 user_id 记录),光靠 ON 不够,必须在子查询里先 GROUP BY 或用 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) 取最新一条。
最容易被忽略的三个细节
真正难的不是写出子查询,而是判断哪张表该聚合、按什么字段聚合、是否要加业务过滤——这些都得贴着业务逻辑抠。
- 子查询里漏了个
AND deleted = 0,结果还是错的 - 聚合子查询的关联字段(如
user_id)没索引,JOIN 时退化为全表扫描 - 跨库或大宽表场景下,不能写
LEFT JOIN,得在应用层分别查出两个 Map,再用user_id做 key 合并











