GROUP BY后SUM/COUNT翻倍是因JOIN导致数据膨胀,非GROUP BY错误;需将右表过滤条件移至ON、用子查询预聚合“多”侧表,并确保GROUP BY字段覆盖语义主键及索引对齐。

为什么GROUP BY后SUM/COUNT翻倍?
不是GROUP BY算错了,是它执行前的JOIN已经把数据撑爆了。SQL执行顺序是FROM → JOIN → WHERE → GROUP BY → SELECT,一旦orders和order_items一对多、又没控好右表基数,1个订单 × 5条明细就会生成5行物理记录——SUM(amount)自然加5遍,COUNT(*)也变成5。
查EXPLAIN时重点关注:rows列是否远超主表行数;MySQL出现Using temporary / filesort;PostgreSQL看到Nested Loop (Join Filter: true)或External sort。这些都不是GROUP BY的锅,是JOIN阶段失控的信号。
LEFT JOIN中WHERE和ON放错位置等于主动造膨胀
这是最常踩的坑。过滤右表字段(比如customers.status)必须写进ON,不能塞WHERE里:
-
错误写法:
LEFT JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active'→ 先全量拉所有客户(含NULL和非active),再过滤,中间已膨胀 -
正确写法:
LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active'→ 右表只取active记录,从源头压窄结果集 - 如果业务要求保留无客户匹配的订单,
WHERE里绝不能碰右表字段,否则LEFT JOIN直接退化成INNER JOIN
子查询预聚合才是根治方案
别指望DISTINCT或ANY_VALUE()糊弄过去。DISTINCT对SUM无效(SUM(DISTINCT amount)是去重金额值,不是去重行);真正要做的,是让“多”侧表先按关联键压缩成单行,再和主表拼接。
- 对
order_items:用(SELECT order_id, SUM(amount) AS total FROM order_items WHERE status = 'paid' GROUP BY order_id)—— 过滤和聚合都放在子查询内 - 对
customers:如果只取name,就别LEFT JOIN customers,改用(SELECT id, name FROM customers WHERE status = 'active') - 子查询外层千万别加
ORDER BY或LIMIT,优化器可能无法下推,导致全表扫 - 千万确认子查询
GROUP BY字段和外层ON字段严格一致,漏了就变交叉连接
GROUP BY字段必须覆盖语义主键,且索引要对齐
你想按用户统计,但JOIN了orders、addresses、profiles三张表,GROUP BY user_id远远不够——只要其中一张表对user_id不唯一(比如一个用户有3个地址),中间结果就至少膨胀3倍。
这时要么:
- 把所有参与
JOIN的“一”侧维度字段都放进GROUP BY,比如GROUP BY u.id, u.name, a.addr_type(但注意这已不是“用户维度”,而是“用户+地址类型”维度) - 或者更稳:用子查询分别聚合每张“多”侧表,再
LEFT JOIN回来,确保每张表对user_id输出至多1行
索引也得配合:如果查询是WHERE shop_id = 123 GROUP BY user_id,光有user_id索引没用,最优是(shop_id, user_id)联合索引——前段快速定位数据块,后段让该块内数据天然有序,避免filesort。
真正难的不是写出子查询语法,而是判断哪张表该聚合、按什么字段聚合、要不要加deleted = 0或时间范围——这些细节一漏,结果还是错的。

















