GROUP BY 不能保证返回每组“第一条”记录,因其仅定义分组逻辑而不指定组内顺序;可靠方案是使用窗口函数 ROW_NUMBER() 配合 PARTITION BY 和显式 ORDER BY 筛选 rn = 1 的行。

GROUP BY 本身不能保证返回每组“第一条”记录
SQL 标准中,GROUP BY 只定义分组逻辑,不定义组内行的顺序或选取规则。如果你写了 SELECT col1, col2 FROM t GROUP BY key 却没用聚合函数(如 MAX()、MIN()),多数数据库(如 PostgreSQL、SQL Server)会直接报错;MySQL 在旧版本(5.7 严格模式关闭时)可能“随机”返回某一行,但这个行为不可靠、不可移植、不推荐依赖。
用窗口函数 ROW_NUMBER() 是最通用可靠的方案
核心思路:先按分组字段和排序字段打序号,再筛选序号为 1 的行。这适用于 PostgreSQL、SQL Server、Oracle、SQLite 3.25+、MySQL 8.0+ 等主流现代数据库。
常见写法:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col) AS rn
FROM your_table
) t WHERE rn = 1;注意点:
-
PARTITION BY group_col对应你要的分组依据(比如user_id、category) -
ORDER BY sort_col决定哪条算“第一条”——是最新时间?最小 ID?最高分数?必须显式指定,否则结果不确定 - 如果存在并列(如相同
sort_col值),ROW_NUMBER()仍会强制编号(1,2,3…),而RANK()或DENSE_RANK()会产生重复序号,通常不适用此场景
MySQL 5.7 或更老版本没有窗口函数?用相关子查询模拟
在不支持 ROW_NUMBER() 的环境中,可用子查询找每组满足条件的最小/最大主键或时间戳,再关联回原表。性能较差,且需确保排序字段有索引。
示例(取每组 user_id 下 created_at 最新的记录):
SELECT t1.* FROM your_table t1 WHERE t1.created_at = ( SELECT MAX(t2.created_at) FROM your_table t2 WHERE t2.user_id = t1.user_id );
潜在问题:
- 若同一
user_id有多条相同最大created_at,会返回多行 —— 这不是“第一条”,而是“所有最新行” - 想严格只取一条,得加额外去重逻辑(比如再比
id),语句迅速变复杂 - 无索引时,子查询可能全表扫描,大数据量下明显变慢
别把 GROUP BY + ANY_VALUE() 当“取第一条”用
MySQL 5.7+ 开启了 ONLY_FULL_GROUP_BY(默认开启)后,非聚合字段必须出现在 GROUP BY 中,否则报错。有人用 ANY_VALUE(col) 绕过,例如:
SELECT user_id, ANY_VALUE(name), MAX(created_at) FROM t GROUP BY user_id;
但要注意:
-
ANY_VALUE()不保证返回哪一条,只是“随便挑一个”,和你的业务要求的“第一条”(比如最新、最早、ID 最小)常常不一致 - 它本质是放弃一致性保障,属于妥协方案,仅适合对具体值完全不敏感的统计场景
- 换数据库就失效(PostgreSQL、SQL Server 没这个函数)
真正需要确定性结果时,窗口函数是事实标准。最容易被忽略的是 ORDER BY 子句里没写清楚排序依据——很多人只写 PARTITION BY a,忘了 ORDER BY b,结果“第一条”每次执行都可能不同。

















