join在聚合前执行导致行数膨胀,sum重复累加;唯一可靠解法是先对多端表按关联键预聚合再join,或用sum() over(partition by主表唯一键)避免行膨胀。

JOIN在聚合前执行,行数先膨胀再SUM
SQL执行顺序决定了问题根源:JOIN发生在SELECT和SUM()之前。哪怕你只写SELECT u.id, SUM(o.amount),数据库也必须先把users和orders完整JOIN出来——一个用户有3笔订单,u.id和u.name就会在中间结果里出现3次,SUM(o.amount)就真把这3行的amount全加一遍。这不是bug,是标准行为。
子查询预聚合是最稳妥的修复方式
核心是把聚合“上提”到JOIN之前,切断主表行被复制的链条:
- 子查询里必须
GROUP BY右表的外键(如user_id),否则无法压缩成单行 - 外层
ON条件必须和子查询GROUP BY字段严格一致,类型也要匹配(比如都是INT,不能一边VARCHAR一边INT) - 用
LEFT JOIN时,记得COALESCE(ord.total_amount, 0)处理NULL,否则SUM(NULL)返回NULL而非0 - 错误写法:
SELECT u.id, SUM(o.amount) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id——它在膨胀后的结果上硬SUM,结果必然翻倍
正确写法示例:
SELECT u.id, u.name, COALESCE(ord.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 ) ord ON u.id = ord.user_id;
SUM() OVER适合需要明细+汇总共存的场景
当你既要每行显示订单详情,又要带本订单总金额(比如“订单号#123,商品A,金额50,本订单合计280”),SUM() OVER比JOIN更干净:
-
PARTITION BY必须选主表唯一字段(如order_id),确保每个逻辑单位只算一次 - 它不改变行数,不会引入重复,但也不能解决“从关联表取COUNT(DISTINCT xxx)”这类需求——窗口函数无法自动去重行
- MySQL 5.7及更早版本不支持
OVER,必须降级为相关子查询或预聚合
示例:
SELECT o.order_id, oi.product_name, oi.price, SUM(oi.price) OVER (PARTITION BY o.order_id) AS order_total FROM orders o JOIN order_items oi ON o.order_id = oi.order_id;
DISTINCT对SUM基本无效,别碰它
SUM(DISTINCT amount)不是去重“行”,而是去重“数值”。一笔100元订单和另一笔100元订单会被当成同一个值,直接漏加;而同一订单的多条明细即使价格不同,也会被全部计入——完全违背业务语义。
-
COUNT(DISTINCT id)可以用于统计条数,但SUM(DISTINCT ...)在金额类字段上几乎无合理使用场景 - 用
DISTINCT包裹整行或加GROUP BY字段,只是掩盖问题,执行计划里rows_examined依然暴增,索引可能失效 - 真正要处理的是JOIN逻辑本身,不是在
SUM函数里调参数
最容易被忽略的细节:预聚合子查询的GROUP BY字段和外层ON条件没对齐,或者忘了COALESCE——这些地方不出错则已,一出错就是全盘失准,且很难通过肉眼查数据发现。











