LEFT JOIN后SUM翻倍是因右表一对多导致左表行被复制,聚合函数在膨胀后的行上重复计算;正确解法是用子查询按关联键预聚合右表再JOIN,或用ROW_NUMBER()取单条记录。

LEFT JOIN后直接写SUM()会放大结果
因为LEFT JOIN本身不控制右表匹配行数,一旦右表对同一个关联键有多条记录,左表那行就会被复制多次——SUM()、COUNT()这些聚合函数就跟着算多次。比如用户表1行 + 订单表3行匹配,JOIN后变成3行,SUM(amount)就把同一笔订单加了3遍。
常见错误现象:SUM(orders.amount) 返回值比实际总金额高几倍;COUNT(*) 明显大于左表原始行数;用 DISTINCT 包不住,因为右表带了不同时间戳或ID,整行不重复。
- 聚合函数必须作用在“已按业务主键归一化”的数据集上,不能让明细行直接参与外层聚合
- ON条件里加聚合(如
ON u.id = o.user_id AND SUM(o.amount) > 100)语法非法,数据库直接报错 - WHERE里用聚合也不行,因为WHERE执行阶段还没分组,
HAVING又只能跟在GROUP BY之后,无法用于LEFT JOIN的关联逻辑
子查询是唯一能提前控制右表粒度的方式
你不能靠外层GROUP BY“补救”,那只是把膨胀后的结果再压回去,既慢又容易选错值(比如MAX(created_at)和对应行的amount不匹配)。真正可靠的解法,是在JOIN之前就把右表按 user_id 这类业务主键聚好,确保每组只出1行。
例如统计每个用户的订单总额:
SELECT u.id, u.name, COALESCE(t.total_amount, 0) AS total_amount FROM users u LEFT JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id ) t ON u.id = t.user_id;
- 子查询里的
GROUP BY user_id是硬性要求,漏掉就会返回多行,JOIN后照样膨胀 -
COALESCE(t.total_amount, 0)比IFNULL更通用,避免无订单用户字段为NULL导致前端报错 - 如果右表有状态过滤(如
status = 'paid'),一定要写在子查询内部,而不是外层WHERE——否则先JOIN再过滤,已经膨胀过了
为什么不能把聚合写在ON或WHERE里?
ON子句只接受布尔表达式,不接受聚合函数;WHERE在JOIN完成后才执行,此时行已经复制完毕。试图绕过子查询,比如写成 LEFT JOIN orders o ON u.id = o.user_id WHERE SUM(o.amount) > 100,数据库会直接拒绝:Unknown column 'o.amount' in 'where clause' 或 Invalid use of group function。
- MySQL 5.7+ 严格模式下,子查询里非分组字段必须套聚合函数,如
MAX(order_no),不能裸写 - PostgreSQL 要求所有SELECT列表字段要么在GROUP BY中,要么被聚合包裹,否则报错
- 哪怕你只想要一个任意值(比如任一条订单号),也得用
ANY_VALUE(order_no)或MIN(order_no),不能省
窗口函数ROW_NUMBER()适合要完整行的场景
当你要的不只是SUM或COUNT,而是最新一条订单的全部字段(order_no, amount, created_at),子查询+GROUP BY就不够用了。这时得用 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) 先标序,再筛 rn = 1。
注意:MySQL 5.7 不支持窗口函数,必须升到8.0+;SQL Server 用 OUTER APPLY 更自然;PostgreSQL 支持LATERAL,但ROW_NUMBER()方案更跨平台。
- ORDER BY里必须有确定性排序字段(如时间戳+ID),否则
rn = 1结果不可控 - 不要在子查询外再加ORDER BY,窗口函数里的排序已决定逻辑顺序
- 如果右表数据量极大,记得在
user_id和排序字段上建联合索引,否则PARTITION BY + ORDER BY会全表扫描

















