翻倍源于JOIN导致的一对多数据膨胀,需先验证右表关联字段是否存在重复;根治方法是用子查询预聚合右表,GROUP BY关联字段并把过滤条件写入子查询内。

GROUP BY 与 JOIN 联查后 SUM 或 COUNT 翻倍,不是 SQL 写错了,而是 JOIN 阶段已经把数据复制膨胀了——GROUP BY 只是“照着错的行数算”,改 GROUP BY 本身毫无意义。
先确认是不是一对多导致的膨胀
别急着改 SQL,先验证问题根源。执行以下语句检查被 JOIN 的右表是否在关联字段上存在重复:
SELECT join_column, COUNT(*) FROM right_table GROUP BY join_column HAVING COUNT(*) > 1- 如果返回结果不为空,且某
join_column对应的COUNT(*)远大于COUNT(DISTINCT left_table.primary_key),基本可锁定是一对多膨胀 - 看执行计划:
EXPLAIN中若rows_examined显著高于左表总行数(比如左表 1 万行,JOIN 后预估扫描 50 万行),就是典型信号 - 右表如果是视图或子查询,把它单独执行一遍,看输出里是否有重复
join_column
用子查询预聚合,而不是在 JOIN 后 GROUP BY
这是唯一能根治翻倍的写法。核心是让“多”侧表先按关联键压缩成一行,再和主表拼接,避免中间结果出现重复行。
- 子查询必须
GROUP BY关联字段,且该字段要和外层ON条件里的右表字段完全一致,例如子查询按order_id分组,外层就得写ON o.id = oi.order_id,不能写成ON o.id = oi.id - 过滤条件(如
status = 'paid')必须写在子查询内部,不能挪到外层WHERE,否则子查询仍会包含无效行 -
LEFT JOIN未匹配时,聚合字段为NULL,SUM(NULL)返回NULL,需用COALESCE(SUM(), 0)显式转 0 - 子查询必须有别名,否则 MySQL 报
Error Code: 1248
SELECT u.id, u.name, COALESCE(o.total_amount, 0) AS total_amount FROM users u LEFT JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE status != 'cancelled' -- 过滤必须放这里 GROUP BY user_id -- 必须和 ON 字段严格对齐 ) o ON u.id = o.user_id;
LEFT JOIN 中 ON 和 WHERE 放错位置会掩盖膨胀
这不是风格问题,是执行逻辑差异:放 WHERE 是全量 JOIN 完再过滤,膨胀已发生;放 ON 是从源头控制右表参与连接的行数。
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→ 先拉所有订单,再剔除非 paid 行,中间已复制膨胀 - 正确写法:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 右表只取满足条件的行,无额外复制 - 如果业务要求保留无订单用户,又只统计已支付订单,
WHERE绝对不能碰右表字段,否则LEFT JOIN退化成INNER JOIN
窗口函数适合需要明细+汇总共存的场景
当你要在每条明细行上显示“所属主表的汇总值”(比如每条订单明细旁显示本订单总金额),用 SUM() OVER 比传统 JOIN 更干净,不改变行数,也不引入重复。
-
SUM(price) OVER (PARTITION BY order_id)不会放大行数,但SUM() GROUP BY会压缩行数——选哪个取决于你要输出的维度 - 如果还需关联主表字段(如订单创建时间),建议先用窗口函数算好汇总,再把这个结果
LEFT JOIN主表,而不是反过来 - 注意老版本 MySQL(
SELECT oi.order_id, oi.price,
SUM(oi.price) OVER (PARTITION BY oi.order_id) AS order_total
FROM order_items oi;最容易被忽略的是:翻倍问题永远出在 JOIN 阶段,而不是 GROUP BY 或 SUM 函数本身。盯着 SUM() 加 DISTINCT 或调参数,只会让逻辑更混乱、性能更差。真正该花时间的,是看清执行计划里的 rows 是怎么跳变的,以及右表的数据分布到底长什么样。

















