group by本身不产生重复行,常见误解源于select列不满足函数依赖:mysql 5.7+默认启用only_full_group_by,要求非聚合列必须出现在group by中或用any_value()等包裹,否则报错;真正去重应优先用distinct、distinct on(postgresql)或窗口函数row_number()。

GROUP BY 本身不产生重复行,但常见误解来自 SELECT 列不满足函数依赖
很多人以为 GROUP BY 后“出现了重复数据”,其实是 SELECT 子句里写了非分组列且没加聚合函数,导致数据库报错(如 MySQL 5.7+ 的 sql_mode=only_full_group_by)或返回不确定值(如旧版 MySQL 或 SQLite)。真正需要“去重”的场景,往往不是 GROUP BY 本身的问题,而是:你本想按某几列分组,但原始数据里这些列组合就有重复行,而你只想保留其中一条。
用 DISTINCT ON(PostgreSQL)最直接解决“每组取一行”需求
PostgreSQL 提供 DISTINCT ON,它能按指定列分组并取每组第一条(需配合 ORDER BY 控制“哪条是第一”)。这不是标准 SQL,但对这类需求极其精准:
SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESC;
这表示:按 user_id 分组,每组只取 created_at 最大的那条记录。注意两点:
-
DISTINCT ON的字段必须是ORDER BY的前缀,否则报错ERROR: SELECT DISTINCT ON expressions must match initial ORDER BY expressions - 它不等价于
GROUP BY—— 不支持聚合计算,纯属“去重取样” - 如果只要某几列(比如
user_id, status),就写SELECT DISTINCT ON (user_id) user_id, status,别写*再加其他非分组列
用窗口函数 ROW_NUMBER() 实现跨数据库的“每组取一”逻辑
MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持,兼容性比 DISTINCT ON 更广。核心是给每组内行编号,再筛出编号为 1 的:
SELECT user_id, order_id, amount
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;
关键细节:
-
PARTITION BY定义分组依据,必须和你要“去重”的维度一致(如多个字段就写PARTITION BY region, category) -
ORDER BY在窗口内决定谁排第一;若排序字段有重复值,ROW_NUMBER()仍会强制编号(1,2,3…),而RANK()或DENSE_RANK()会产生并列,此时可能返回多行——除非你明确要并列首名 - 性能上,窗口函数通常比自连接或子查询更高效,但需确保
PARTITION BY和ORDER BY字段有索引(例如(user_id, created_at)复合索引)
误用 GROUP BY + 非聚合列导致“看似重复”的典型错误
执行类似下面的语句时,MySQL 5.7+ 直接报错,而旧版或某些配置下会静默返回任意一行,让人误以为“结果重复”:
SELECT user_id, order_id, amount FROM orders GROUP BY user_id;
问题在于:order_id 和 amount 不在 GROUP BY 中,也没用 MIN()/MAX() 等聚合函数包裹。正确做法只有两种:
- 补全聚合逻辑:比如你要每用户最新订单的金额,就写
MAX(created_at)配合子查询,或改用上面的窗口函数 - 确认是否真需要 GROUP BY:如果只是去重,且所有列都参与判断,直接用
SELECT DISTINCT user_id, order_id, amount即可 - 别依赖 MySQL 的“隐式分组”行为——它已被标记为过时,未来版本会彻底移除
真正难处理的,是分组键本身存在语义重复(比如两个 user_id 其实指向同一人),那得先做数据清洗或主数据匹配,SQL 层面无解。










