最可靠方式是用row_number()配合partition by和order by生成序号,再通过子查询或cte外层筛选rn=1;它能精确返回每组排序后首行完整记录,而max()+group by仅得字段值、丢失整行数据。

用 ROW_NUMBER() 窗口函数取每组 Top 1
最可靠、兼容性较好的方式是用 ROW_NUMBER() 配合 OVER (PARTITION BY ... ORDER BY ...)。它明确按指定排序生成序号,再筛选序号为 1 的行。
常见错误是直接 GROUP BY + MAX(),但这只能拿到最大值,拿不到对应整行数据(比如姓名、时间戳等其他字段会错乱)。
- 必须用子查询或 CTE 包裹窗口函数结果,再在外层
WHERE rn = 1 -
ORDER BY里要写清楚主次排序逻辑,比如先按score DESC,分数相同时用id ASC保证确定性 - 注意
NULL值默认排在最前(ASC)或最后(DESC),必要时加NULLS LAST(PostgreSQL)或用COALESCE()处理
SELECT id, user_id, score, created_at
FROM (
SELECT id, user_id, score, created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY score DESC, id ASC
) AS rn
FROM submissions
) t
WHERE rn = 1;
MySQL 8.0+ 可用 FIRST_VALUE() 但不推荐直接取整行
FIRST_VALUE() 能取某列的首个值,但它不能直接返回整行记录——你得为每一列单独套一次函数,既啰嗦又难维护,还容易因 NULL 或类型隐式转换出错。
更关键的是:如果排序依据列有重复值,FIRST_VALUE() 不保证稳定返回同一行(取决于执行计划),而 ROW_NUMBER() 显式加 id ASC 就能控制。
- 别写
SELECT FIRST_VALUE(id), FIRST_VALUE(score), ...—— 这不是“取第一行”,而是对每列分别取第一个值,可能来自不同物理行 - 若坚持用,必须配合
OVER (PARTITION BY ... ORDER BY ...),且所有FIRST_VALUE()的ORDER BY子句完全一致 - 性能上无优势,可读性和可靠性反而更低
旧版 MySQL(5.7 及之前)没有窗口函数怎么办
只能靠相关子查询或左连接模拟,但性能差、易出错,尤其数据量稍大就明显变慢。
典型写法是:对每条记录,查同组中是否有“排序更优”的其他记录;如果没有,说明它是第一。
- 子查询版本:外层
WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.group_id = t1.group_id AND (t2.score > t1.score OR (t2.score = t1.score AND t2.id - 左连接版本:连自己,条件是
t1.group_id = t2.group_id AND (t2.score > t1.score OR (t2.score = t1.score AND t2.id ,再 <code>WHERE t2.id IS NULL - 务必给
(group_id, score, id)建联合索引,否则全表扫描不可避免
ORDER BY 中的 NULL 和重复值处理常被忽略
真实数据里 score 是 NULL 很常见,而不同数据库对 NULL 在 ORDER BY 中的默认位置不一致:MySQL 默认 NULL 最小,PostgreSQL 默认 NULLS FIRST,SQL Server 默认 NULL 最小但可配。
如果不显式声明,同一语句在不同环境可能返回不同“第一名”。更麻烦的是,当排序字段存在大量重复值(比如多个用户都是满分),ROW_NUMBER() 仍会强行编号,但业务上可能需要“并列第一”——这时就得换 RANK() 或 DENSE_RANK(),并接受多行 rn = 1。
- 明确写
ORDER BY score DESC NULLS LAST(PostgreSQL/Oracle)或ORDER BY COALESCE(score, -999999) DESC(MySQL) - 用
RANK()时,注意它跳过后续名次(两个第一,下一个就是第三),DENSE_RANK()则连续(两个第一,下一个是第二) - 如果业务要求“只取一条”,哪怕并列也强制选一个,就必须在
ORDER BY里加唯一字段兜底,比如id或created_at











