SQL聚合翻倍是因JOIN在聚合前将主表一行复制为N行,导致SUM重复累加N遍;须先查右表关联字段重复、验证执行计划rows突增、确保过滤条件写在ON中、用子查询预聚合并严格对齐GROUP BY与JOIN字段,同时用COALESCE处理NULL。

因为JOIN在聚合之前执行,把主表一行物理复制成N行(N=关联子表匹配行数),SUM就真的一行一行加了N遍——这不是SQL算错,是它老老实实按中间结果集算出来的。
查右表关联字段是否重复
翻倍的根源永远在JOIN逻辑本身,不是聚合函数写错了。先别改SQL,直接验证右表对左表主键是否唯一:
- 执行
SELECT join_column, COUNT(*) FROM right_table GROUP BY join_column HAVING COUNT(*) > 1,有返回就坐实一对多 - 对比
COUNT(*)和COUNT(DISTINCT left_table.primary_key):前者远大于后者,就是膨胀信号 - 看执行计划:
EXPLAIN中某步rows突增几倍甚至几十倍(比如orders表1万行,JOIN后预估50万行),基本锁定是JOIN膨胀
LEFT JOIN里WHERE和ON写错位置
这是最隐蔽也最常踩的坑:放WHERE是先全量JOIN再过滤,放ON才是从源头控制右表参与连接的行数。
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→ 先拉所有订单进来,再筛,中间已膨胀 - 正确写法:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 右表只取已支付订单,没膨胀 - 如果业务要求保留无订单用户,WHERE里绝对不能碰右表字段,否则LEFT JOIN退化成INNER JOIN
用子查询预聚合替代JOIN后聚合
这是唯一能根治翻倍的解法,核心是让“多”侧表先按关联键压缩成单行,再和主表JOIN。
- 子查询必须
GROUP BY关联字段(如user_id),漏掉等于白做 - 子查询必须有别名(如
t),否则MySQL报Error Code: 1248 - 过滤条件(如
WHERE status = 'paid')必须写在子查询内部,不能挪到外层 - LEFT JOIN未匹配时,聚合字段为
NULL,SUM(NULL)返回NULL而非0,得用COALESCE(SUM(), 0)显式处理
窗口函数SUM() OVER能绕过但有局限
SUM() OVER (PARTITION BY main_table.unique_key) 在JOIN之后执行,但靠 PARTITION BY 让它按逻辑分组独立求和,不依赖行数压缩。
- 适用场景:需要每行都显示汇总值(比如每条订单明细旁显示“本订单总金额”)
-
PARTITION BY必须选主表中真正唯一的字段(如order_id),否则仍会错 - 不能解决“从关联表字段出发的去重统计”,比如
COUNT(DISTINCT oi.product_id) OVER (...)依然会因行膨胀重复计数 - MySQL 5.7及以前不支持
OVER,必须改用相关子查询或预聚合
最容易被忽略的是:子查询的 GROUP BY 字段和外层 ON 条件没对齐,或者忘了用 COALESCE 处理 NULL——这些细节不显眼,但一出错就全盘失准。










