GROUP BY 发生在 JOIN 之后,导致右表不唯一时数据膨胀;正确做法是将右表过滤条件写入 ON 子句或用子查询预聚合,并严格按关联字段分组。

因为 GROUP BY 发生在 JOIN 之后,而 JOIN 已经把主表一行复制成了多行——聚合函数只是老老实实对着这些物理行算,不是按业务逻辑“去重后算”。
翻倍不是数据库错了,是执行顺序决定的
SQL 执行顺序是 FROM → JOIN → WHERE → GROUP BY → SELECT。一旦右表对左表主键不唯一(比如一个用户有 3 笔订单),JOIN 阶段就生成了 3 行;GROUP BY user_id 只是对这 3 行分组,SUM(amount) 就真加了 3 次。
- 查执行计划时重点关注
rows_examined:如果orders表本身只有 1 万行,但JOIN后扫描了 40 万行,基本就是膨胀了 -
EXPLAIN中看到type: ALL+Extra: Using join buffer,大概率是ON条件漏写或没走索引 - 别只看
GROUP BY后的行数——那已经晚了,错误发生在它之前
LEFT JOIN 中 WHERE 和 ON 放错位置,会让问题更隐蔽
把右表过滤条件写在 WHERE 里,等于先让所有右表行参与 JOIN,再砍掉不符合的——膨胀已发生。而写在 ON 里,是从源头控制右表参与连接的行数。
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid'→LEFT JOIN退化成INNER JOIN,且中间已膨胀 - 正确写法:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'→ 右表只拉出status = 'paid'的记录,没膨胀 - 如果业务要求保留无订单的用户,又只取已支付订单,
WHERE绝对不能碰右表字段
子查询预聚合才是根治方案,但细节一错全错
核心思路是让“多”侧表先按关联键压缩成单行,再和主表拼接。这不是加个 DISTINCT 能糊弄过去的。
- 子查询必须严格
GROUP BY关联字段,比如按user_id聚合,外层ON就得是u.id = o.user_id,不能写成u.id = o.id - 业务过滤条件(如
WHERE status = 'paid')必须放在子查询内部,放外层WHERE会导致子查询输出仍含无效行 -
LEFT JOIN未匹配时,聚合字段是NULL,SUM(NULL)返回NULL,要用COALESCE(SUM(), 0)显式转 0
真正难的不是写出子查询,而是判断哪张表该聚合、按什么字段聚合、是否要加时间范围或状态过滤——这些都得贴着业务逻辑抠,漏一个,结果照样错。

















