不能直接用 GROUP BY 配合 MAX() 拿到完整行,因为 GROUP BY 压缩多行成一行,MAX() 只返回聚合值,其他字段无法保证来自同一原始行;RANK() 是窗口函数,在每个分区中按指定顺序排名,不改变行数,需用子查询或 CTE 过滤 rk=1,且要注意索引、NULL 处理及数据库版本支持。

为什么不能直接用 GROUP BY 配合 MAX() 拿到完整行
因为 GROUP BY 会压缩多行成一行,而 MAX() 只返回聚合值,其他字段(比如 id、name、created_at)无法保证来自同一原始行——数据库没义务把 MAX(score) 对应的那条记录的 user_id 也顺手捞出来。常见错误是写成:SELECT category, MAX(score), user_id FROM records GROUP BY category,结果 user_id 是任意一条的,不一定是最高分那条的。
RANK() OVER (PARTITION BY ... ORDER BY ...) 是怎么工作的
RANK() 是窗口函数,它在每个分区(如每个 category)内按指定顺序(如 score DESC)给行打序号:相同值并列,跳过后续名次(比如两个第一,下一个就是第三)。关键在于它不改变行数,每行都保留原字段,只多一列排名。
实操建议:
- 必须搭配
OVER子句,PARTITION BY category划分组,ORDER BY score DESC决定谁排前面 - 如果分数可能重复且你只要一条(哪怕并列),改用
ROW_NUMBER();如果接受并列且要全留下,就用RANK() - 别漏写
DESC,否则最低分变“第一名”
示例(查每个分类下最高分的完整记录):
SELECT * FROM ( SELECT *, RANK() OVER (PARTITION BY category ORDER BY score DESC) AS rk FROM records ) t WHERE rk = 1;
WHERE 不能直接用 RANK(),必须套子查询或 CTE
窗口函数不能出现在 WHERE 子句里,因为 SQL 执行顺序是 WHERE → GROUP BY → SELECT → WINDOW FUNCTION,WHERE 阶段 RANK() 还没算出来。硬写 WHERE RANK() = 1 会报错:ERROR: window functions are not allowed in WHERE。
正确做法只有两种:
- 用子查询(如上例),把带
RANK()的结果当临时表再过滤 - 用 CTE(更易读):
WITH ranked AS (SELECT *, RANK() OVER (...) AS rk FROM records) SELECT * FROM ranked WHERE rk = 1
别试图在外部查询加 HAVING——HAVING 是配 GROUP BY 的,这里没分组。
性能和 NULL 值要注意什么
大表上跑 RANK() 可能慢,尤其没索引时。优化点:
- 在
PARTITION BY和ORDER BY字段建联合索引,例如CREATE INDEX idx_category_score ON records(category, score DESC) -
NULL默认排最前(ORDER BY ... DESC时),如果score允许为空,且你不想让NULL占“第一名”,得显式排除:WHERE score IS NOT NULL再进窗口计算 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 支持,但旧版不支持,别在老环境硬用
真正容易被忽略的是:RANK() 并列时会返回多行,如果你业务上要求“每个分类只取一条”,哪怕分数相同,也得切到 ROW_NUMBER() 并补一个次级排序(比如 ORDER BY score DESC, id ASC)来打破并列。

















