LEFT JOIN后直接SUM()翻倍是因关联导致行复制,唯一可靠解法是预聚合:对右表按关联键GROUP BY后子查询或CTE再JOIN,确保每行主表仅对应一行聚合值。

直接在 LEFT JOIN 后用 SUM() 或 COUNT() 一定会错——不是函数有问题,是数据在聚合前就被复制了。唯一靠谱的解法是:让聚合发生在关联之前。
为什么 SUM() 会翻倍?先查右表有没有重复关联键
别急着改 SQL,先确认是不是一对多惹的祸:
- 执行
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) > 1,如果有结果,说明一个用户真有多条订单 - 再比对:
COUNT(*)和COUNT(DISTINCT users.id),如果前者远大于后者,基本坐实膨胀 - 注意:右表如果是视图或子查询,得单独跑一遍看输出里
user_id是否重复 - 外键没加
UNIQUE约束?用SHOW CREATE TABLE orders看一眼
LEFT JOIN 后直接聚合,为什么不能靠 DISTINCT 补救?
DISTINCT 是最后一步去重,而 SUM()、COUNT() 在它之前就执行完了。行已经复制,聚合值早就放大,DISTINCT 只能删行,不能修正数字。
- 错误写法:
SELECT u.id, COUNT(DISTINCT o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id——这能算对订单数,但换成SUM(o.amount)就不行 -
COUNT(DISTINCT ...)是特例,只对去重后的逻辑值计数;SUM()、AVG()、MAX()都是对所有行原始值运算 - 想靠
HAVING或外层WHERE过滤来“修正”,只会让问题更隐蔽——比如把LEFT JOIN退化成INNER JOIN
用子查询预聚合,三个细节漏一个就白干
核心是:子查询必须返回唯一键 + 聚合结果,且和外层 ON 条件严丝合缝。
-
GROUP BY字段必须和ON中右表字段完全一致,比如子查询按order_id分组,外层就得写ON o.order_id = s.order_id,不能写成ON o.id = s.order_id -
LEFT JOIN没匹配时,SUM()返回NULL,不是0——得显式写COALESCE(SUM(amount), 0) - 业务过滤条件(如
WHERE status != 'cancelled')必须下推到子查询内部,写在外层WHERE里等于没过滤 - 子查询别名不能省:
MySQL会报Error Code: 1248. Every derived table must have its own alias
多个一对多表同时关联时,别用单个 JOIN 一锅端
比如要查客户、销售总额、发票总额,customer LEFT JOIN sale LEFT JOIN invoice 会产生笛卡尔积:一个客户3笔销售 × 4张发票 = 12 行,SUM(invoice.amount) 被累加3次,SUM(sale.amount) 被累加4次。
- 正确做法:每个子表独立预聚合,再分别
LEFT JOIN主表 - 所有时间/状态等过滤条件(如
date )必须写在各自子查询里 - 主查询不需要
GROUP BY——聚合已在子查询完成 - 如果某客户既无销售也无发票,
LEFT JOIN保证他还在结果里,配合COALESCE(..., 0)安全兜底
真正容易被忽略的是:预聚合不是加个 GROUP BY 就完事,而是要把业务逻辑切分清楚——哪些字段只需汇总值,哪些必须保留明细。一旦需要最新订单时间、最高单价这类非分组字段,子查询就扛不住,得切到窗口函数或 LATERAL。

















