ROW_NUMBER()最稳当做法是分组内按目标字段降序排序后取rn=1,可完整保留原始行所有字段;需注意NULL处理、并列值选择RANK()、旧版数据库用关联子查询兜底。

用窗口函数 ROW_NUMBER() 最稳当
直接在分组内按目标字段降序排序,取每组序号为 1 的行,能 100% 拿到完整原始行(包括其他列),且逻辑清晰、兼容主流数据库(PostgreSQL / SQL Server / Oracle / MySQL 8.0+)。
常见错误是用 GROUP BY 配合 MAX(),结果只能拿到最大值和分组键,其他字段值不可靠(MySQL 5.7 默认模式下甚至报错)。
- 写法核心:
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY target_col DESC) - 必须用
DESC确保最大值排第一;如果目标字段可能为NULL,加NULLS LAST(PostgreSQL)或提前过滤 - 别名后在外部查询中过滤
rn = 1,不能在WHERE中直接用窗口函数
SELECT id, name, dept, salary
FROM (
SELECT id, name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn = 1;
当存在并列最大值时,选 RANK() 还是 DENSE_RANK()
如果同一组里有多个相同最大值,ROW_NUMBER() 只返回其中一行(随机),而业务可能要求“全都要”。这时得换排名函数。
-
RANK():并列时跳过后续序号(如 1,1,3),适合“所有并列最大值都算有效结果” -
DENSE_RANK():并列不跳号(如 1,1,2),一般用不到这个场景 - 注意:
RANK()返回的不是“第几个”,而是“排第几”,所以筛选仍用r = 1
SELECT id, name, dept, salary
FROM (
SELECT id, name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS r
FROM employees
) t
WHERE r = 1;
MySQL 5.7 或旧版 SQLite 怎么办?用关联子查询兜底
这些环境不支持窗口函数,但又不能改数据库版本,就得用传统方式:对每组先算出最大值,再用 IN 或 = 关联原表。
- 必须确保关联字段组合能唯一标识一行,否则可能重复或漏行
- 性能较差——尤其数据量大时,子查询可能被反复执行;加复合索引
(dept, salary)能明显缓解 - 如果
salary有重复,=写法只匹配第一行;想全匹配得用(dept, salary) IN (SELECT dept, MAX(salary) ...)
SELECT e1.id, e1.name, e1.dept, e1.salary FROM employees e1 WHERE (e1.dept, e1.salary) IN ( SELECT dept, MAX(salary) FROM employees GROUP BY dept );
别忽略 NULL 和字符排序陷阱
实际数据里 salary 为 NULL 时,ORDER BY ... DESC 默认把它排最前(SQL 标准行为),导致 ROW_NUMBER() = 1 拿到的是空值行,不是真最大值。
- 显式控制:
ORDER BY salary DESC NULLS LAST(PostgreSQL / Oracle),MySQL 和 SQL Server 需用ORDER BY IFNULL(salary, -999999) DESC类似技巧 - 字符型字段(如版本号
'v2.10')直接ORDER BY会按字典序比较,'v2.9' > 'v2.10',得转成语义化排序(如拆段解析或用正则) - 时间字段注意时区,
created_at存的是 UTC 但业务要本地最大值,得先转换再排序
窗口函数本身不难,真正卡住人的永远是数据质量细节和边界 case。











