翻倍不是sql算错了,是它老老实实按物理行算出来的结果——只要右表对左表主键不唯一,1行主表×n行子表=n行中间结果,sum、count自然加n遍;应先查右表关联字段重复、验证执行计划rows突增、确保过滤条件写在on中、用子查询预聚合并严格对齐group by与join字段,同时用coalesce处理null。

翻倍不是SQL算错了,是它老老实实按物理行算出来的结果——只要右表对左表主键不唯一,1行主表 × N行子表 = N行中间结果,SUM、COUNT自然就加了N遍。
先确认是不是一对多导致的翻倍
别急着改SQL,先查右表关联字段有没有重复:
-
SELECT join_column, COUNT(*) FROM right_table GROUP BY join_column HAVING COUNT(*) > 1—— 有结果就说明存在一对多 - 对比
COUNT(*)和COUNT(DISTINCT left_table.primary_key),前者远大于后者,基本锁定是JOIN膨胀 - 看执行计划:
EXPLAIN中rows列如果突增几倍甚至几十倍(比如orders表1万行,JOIN后预估50万行),就是强信号 - 右表是视图或子查询?把它单独执行一遍,检查输出里是否有重复的
join_column
LEFT JOIN中WHERE和ON写错位置会掩盖问题
这不是语义差异,是执行逻辑的根本区别:放WHERE是先全量配对再过滤;放ON是从源头控制右表参与连接的行数。
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→ 先把所有订单都JOIN进来,再筛,中间已膨胀 - 正确写法:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 右表只拉符合条件的行,没膨胀 - 如果业务要求保留无订单的用户,又只取已支付订单,
WHERE里绝对不能碰右表字段,否则LEFT JOIN退化成INNER JOIN
子查询预聚合是最稳妥的解法
核心是让“多”侧表先按关联键压缩成单行,再和主表JOIN。这不是可选项,是必须项。
- 子查询必须
GROUP BY关联字段,且字段名要和外层ON条件严格一致,比如子查询按order_id分组,外层就得写ON o.id = oi.order_id,不能写成ON o.id = oi.id - 过滤条件(如
WHERE status = 'paid')必须写在子查询内部,不能挪到外层WHERE,否则子查询输出仍含无效行 -
LEFT JOIN未匹配时,聚合字段为NULL,SUM(NULL)返回NULL而非0,得用COALESCE(SUM(), 0)显式处理 - 子查询必须有别名(如
t),否则MySQL报Error Code: 1248
窗口函数SUM OVER能绕过行膨胀但有局限
SUM() OVER (PARTITION BY ...) 在JOIN之后执行,但靠PARTITION BY让它按逻辑分组独立求和,不依赖行数压缩。
- 适用场景:需要每行都显示汇总值(比如每条订单明细旁显示“本订单总金额”)
-
PARTITION BY必须选主表中唯一标识字段(如order_id),否则分区不准,结果照样错 - 不能用
ORDER BY在窗口定义里(除非真需要累积和),否则可能影响性能或语义 - 当你要聚合的是从关联表来的字段(如
COUNT(DISTINCT oi.product_id)),SUM OVER无法自动去重,此时仍需子查询预聚合 - 注意兼容性:
OVER在 MySQL 8.0+、PostgreSQL 9.4+ 支持,MySQL 5.7 及以前不支持,得换相关子查询
最容易被忽略的是:问题根源不在聚合函数本身,而在JOIN引入的隐式重复。盯着SUM调参数不如先看执行计划里rows是否异常放大;预聚合子查询里GROUP BY字段和外层ON条件没对齐,或者忘了COALESCE处理NULL,这些细节一出错就全盘失准。










