FIRST_VALUE不能直接获取“组内最大值所在行”,因为它仅返回排序后首行的指定列值,无法返回整行其他字段,也不支持行级过滤;正确做法是用ROW_NUMBER()配合PARTITION BY和ORDER BY生成序号,再WHERE rn=1筛选完整记录。

为什么 FIRST_VALUE 不能直接拿到“组内最大值所在行”
FIRST_VALUE 只是取窗口排序后第一行的指定列值,它不关心这行是否对应最大值——哪怕你按 ORDER BY score DESC 排序,它也只机械地取排在最前面那条记录的 id 或 name,但如果你真正想查的是“分数最高的人的完整信息”,FIRST_VALUE 单独用会漏字段、难扩展,还容易和 LAST_VALUE 混淆。
常见错误现象:FIRST_VALUE(id) OVER (PARTITION BY dept ORDER BY score DESC) 确实能取出每个部门最高分者的 id,但你没法同时拿到他的 score(除非再写一遍)、hire_date 等其他字段;更糟的是,如果最高分有并列,它只随机取一个(取决于排序稳定性),且无法过滤出整行做后续 JOIN 或 WHERE。
- 它返回的是标量值,不是行上下文
- 不能替代子查询或
ROW_NUMBER()做精确行筛选 - Oracle/PostgreSQL 支持
IGNORE NULLS,但 MySQL 8.0+ 不支持该修饰,行为不一致
用 ROW_NUMBER() 替代才是稳妥做法
真正要“获取组内最大值所在行的全部字段”,本质是:先按目标字段降序排序,再给每组第一行打标记,最后过滤。这是 ROW_NUMBER() 的标准使用场景,兼容性好、语义清晰、可控性强。
示例(查每个部门 salary 最高者的完整记录):
SELECT id, name, dept, salary
FROM (
SELECT id, name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC, id ASC) AS rn
FROM employees
) t
WHERE rn = 1;
关键点:
-
ORDER BY salary DESC, id ASC解决并列时的确定性:分数相同时按id升序选最小 ID 者 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 全支持,无版本陷阱
- 若需保留所有并列最高者,改用
RANK()或DENSE_RANK(),但注意它们可能返回多个rn = 1
FIRST_VALUE 的合理使用场景有哪些
它适合“把组内某个极值广播到每行”,而不是“提取某一行”。比如生成报表时,在每行显示本部门最高薪、最低入职时间等聚合参考值。
示例(给每个人附上其部门最高 salary):
SELECT name, dept, salary,
FIRST_VALUE(salary) OVER (PARTITION BY dept ORDER BY salary DESC) AS dept_max_salary,
FIRST_VALUE(hire_date) OVER (PARTITION BY dept ORDER BY hire_date ASC) AS dept_earliest_hire
FROM employees;
注意:
- 必须显式写
ORDER BY,否则结果未定义(SQL 标准要求) - PostgreSQL 默认
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,而 MySQL/SQL Server 默认是ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,影响是否能取到全局极值——建议显式加上ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING避免歧义 - 别用它去构造主键关联逻辑,因为值可能重复且无行唯一性保证
性能与索引注意事项
窗口函数本身不走索引优化,执行计划里通常体现为 WindowAgg + Sort。当数据量大时,PARTITION BY 字段和 ORDER BY 字段的联合索引能显著提速。
- 对
employees(dept, salary)建索引,可避免对每个 dept 再次排序 -
ROW_NUMBER()查询比多次FIRST_VALUE()更省资源:前者一次排序复用,后者若多列不同排序逻辑,可能触发多次排序 - 在 PostgreSQL 中,如果
PARTITION BY基数极高(如百万个 group),考虑是否真需要窗口函数,还是改用 LATERAL JOIN + 子查询更可控
真正难的不是写对语法,而是想清楚你要的是“广播一个值”还是“挑出一行”——这两个意图对应完全不同的函数选型和执行路径。










