group by后出现重复行实为join阶段数据膨胀所致,而非聚合本身造成;需检查on条件有效性、避免恒真条件、修正where与on位置、处理字段类型不一致,并用窗口函数或row_number()从源头控量。

聚合后出现重复行,不是笛卡尔积——它压根没在聚合阶段发生,而是JOIN阶段早就膨胀完了。
为什么GROUP BY后还有重复行?
这是最常见的误判。GROUP BY 本身不会生成笛卡尔积;你看到的“重复”,其实是 JOIN 已经把一行主表数据撑成几十行,再 GROUP BY 也只是对这几十行做聚合,结果行数仍可能远超主表实际数量。
典型现象包括:COUNT(*) 比预期大几倍、SUM(amount) 虚高、执行计划里 Nested Loop (Join Filter: true)(PostgreSQL)或 type: ALL + Using join buffer(MySQL)。
- 检查每个 JOIN 后是否紧跟着有效的
ON子句,且至少含一个左表列和一个右表列(如ON o.user_id = u.id) - 警惕
ON 1=1或ON u.id = u.id这类恒真条件 - 老式逗号语法
FROM a, b必须靠WHERE补关联,漏一个字段等于没写
用COUNT(*) OVER (PARTITION BY)快速定位膨胀源
不改逻辑、不锁表、线上也能跑。加个窗口函数就能看出哪张表把主表“拉爆”了:
SELECT u.id, u.name,
COUNT(*) OVER (PARTITION BY u.id) AS order_cnt
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
如果某 u.id 对应的 order_cnt 动辄几百,而业务上一个用户最多下 5 单,那问题不在 GROUP BY,而在 orders 表本身有重复 user_id 或关联字段含 NULL。
- 查重复:
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) > 1 - 查 NULL:
SELECT COUNT(*) FROM orders WHERE user_id IS NULL - 排序看分布:
ORDER BY order_cnt DESC LIMIT 5一眼揪出最离谱的几条
LEFT JOIN中WHERE和ON放错位置会放大中间结果集
这不是语法错误,但效果等同于笛卡尔积:本该被过滤掉的右表行,因为塞进 WHERE,导致 LEFT JOIN 先全量配对再过滤,内存和 IO 都白花了。
错误写法:LEFT JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active' → 先生成所有 c.status IS NULL 的组合,再干掉
正确写法:LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active' → 只拉符合条件的右表行,从源头控量
- 如果业务需要保留无客户匹配的订单,又只取活跃客户信息,
WHERE绝对不能碰右表字段 - 关联字段类型不一致(如
BIGINTvsVARCHAR)会导致隐式转换,索引失效,间接加剧膨胀
聚合前先收拢“多”侧数据,比DISTINCT更治本
DISTINCT 是在 JOIN 完成后才去重,此时笛卡尔积早已发生;而用 ROW_NUMBER() 在 JOIN 前就为右表每组连接键筛出单条,才是从源头掐断膨胀路径。
SELECT u.id, u.name, o.order_id, o.amount
FROM users u
LEFT JOIN (
SELECT user_id, order_id, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) o ON u.id = o.user_id AND o.rn = 1;
-
ON里写o.rn = 1是必须的,尤其对LEFT JOIN;若写进WHERE,会把无订单的用户整行过滤掉 - 排序字段要反映业务优先级,比如
created_at DESC取最新、is_primary DESC取主用 - 别把
ROW_NUMBER()直接扔进ON条件——语法不支持,也破坏可读性
真正麻烦的从来不是写法,而是你得先确认:那个“重复”的订单 ID,到底是数据脏了,还是业务上真允许多条并存?窗口函数能帮你筛出一条,但掩盖不了上游没清洗的事实。











