join后直接聚合易出错,因一对多关系导致数据行膨胀;应先预聚合再join,或用窗口函数分层计算,右表过滤条件须写入on而非where。

直接在 JOIN 后用 SUM 或 COUNT 做聚合,几乎必然算错——不是函数写错了,是数据在 JOIN 阶段就被重复拉出来了。
为什么 JOIN 后 GROUP BY 会放大 SUM/COUNT
一对多关系会让主表单行在 JOIN 后变成多行。比如 orders LEFT JOIN order_items(1 订单 → N 明细),再 LEFT JOIN payments(1 订单 → M 支付),一条订单就膨胀成 N×M 行。SUM(payments.amount) 就被放大了 N×M 倍,COUNT(*) 也完全不是“订单数”而是“订单-明细-支付组合数”。
检查方法:
- 对每个 JOIN 字段执行
SELECT COUNT(*) FROM table GROUP BY join_column HAVING COUNT(*) > 1,确认是否真唯一 - 看执行计划里
rows列:某步突然跳增 10 倍以上,基本就是膨胀源 -
COUNT(DISTINCT orders.id)正常但SUM(order_items.price)偏大,就是典型信号
先预聚合再 JOIN 是最稳的解法
别让数据库先拼出上千万行再统计,而是在 JOIN 前就把从表按关联键收成一行。子查询或 CTE 就够用,不用建物化视图。
示例(统计每个用户的订单总数和总金额):
WITH user_order_agg AS ( SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_spent FROM orders WHERE created_at >= '2024-01-01' GROUP BY user_id ) SELECT u.user_name, COALESCE(ua.order_count, 0), COALESCE(ua.total_spent, 0) FROM users u LEFT JOIN user_order_agg ua ON u.user_id = ua.user_id;
关键点:
- 子查询里
GROUP BY字段必须和主表 JOIN 字段完全一致(类型、名称、NULL 处理) - LEFT JOIN 后必须用
COALESCE(ua.order_count, 0),否则没订单的用户会丢行 - 过滤条件(如时间范围)要写在子查询内,避免无效数据参与 JOIN
窗口函数适合“既要明细又要聚合”的混合场景
当你需要在结果里同时展示单条明细(如最新支付时间)和聚合值(如该订单所有商品总价),用传统 JOIN+GROUP BY 会冲突;窗口函数能分层计算,互不干扰。
示例(每个订单的最新支付时间 + 所有明细总金额):
SELECT o.id, o.created_at, FIRST_VALUE(p.paid_at) OVER (PARTITION BY p.order_id ORDER BY p.paid_at DESC) AS latest_payment, SUM(oi.price) OVER (PARTITION BY oi.order_id) AS total_item_amount FROM orders o LEFT JOIN LATERAL ( SELECT order_id, paid_at FROM payments WHERE order_id = o.id ORDER BY paid_at DESC LIMIT 1 ) p ON true LEFT JOIN order_items oi ON oi.order_id = o.id;
注意:
-
FIRST_VALUE()和SUM() OVER在不同粒度独立运行,不互相污染 -
LATERAL确保每条订单只取一条支付记录,从源头掐断膨胀 - 窗口函数不能出现在
WHERE或ON中,也不能和普通聚合混用在同一级SELECT
LEFT JOIN 中的 WHERE 条件会悄悄转成 INNER JOIN
这是最容易被忽略的逻辑陷阱。写 LEFT JOIN users ON o.user_id = u.id WHERE u.status = 'active',表面是左连,实际效果等同于 INNER JOIN——因为 WHERE 会把 u.status IS NULL 的行全干掉。
正确做法:
- 右表过滤条件一律写进
ON:LEFT JOIN users u ON o.user_id = u.id AND u.status = 'active' - 如果既要保留全部订单,又要只关联活跃用户信息,就只能靠预聚合或子查询提前筛好右表
- 上线前必须验证:加/不加该条件时,主表行数是否一致
真正难的不是语法,是每次写 JOIN 前都得问一句:这张被连的表,按关联字段是否业务唯一?如果不唯一,我是在压缩数据,还是在制造副本?










