FIRST_VALUE() 不总是返回分组第一条,因其默认窗口帧为 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,仅覆盖当前行及之前行;需显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 并确保 ORDER BY 列具备区分度。

为什么 FIRST_VALUE() 不总是返回“分组第一条”
因为 FIRST_VALUE() 默认的窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,它只看当前行及之前的数据——不是整个分组。如果你没显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,结果会随 ORDER BY 位置漂移,尤其在重复排序值时容易出错。
常见错误现象:FIRST_VALUE(name) 在分组内返回了中间某条记录,甚至和 LAST_VALUE() 一样。
- 必须写全窗口定义:
OVER (PARTITION BY group_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) -
ORDER BY列要有足够区分度;如果可能重复,加个唯一列兜底,比如ORDER BY created_at, id - MySQL 8.0+、PostgreSQL、SQL Server 支持该语法;SQLite 3.25+ 也支持,但旧版不认
ROWS子句
FIRST_VALUE() 和 MIN()/MAX() 的本质区别
MIN() 和 MAX() 是聚合函数,按 PARTITION BY 分组后直接算极值;FIRST_VALUE() 是窗口函数,返回的是“排序后第一个原始行的某个字段值”,保留原始结构(比如带时间戳的完整记录)。
使用场景:你想取每组最早创建的那条记录的 status 和 user_id,而不是单纯找最小 status 字符串(字典序 vs 时间序)。
-
MIN(created_at)返回时间值;FIRST_VALUE(user_id) OVER (... ORDER BY created_at)返回对应那条记录的user_id - 不能用
MIN()替代FIRST_VALUE()去拿关联字段,除非你额外JOIN回原表 - 性能上,
FIRST_VALUE()通常比自连接或子查询更轻量,但窗口帧范围越大,内存开销越高
替代方案:用 ROW_NUMBER() 更可控
当你要的“第一条”逻辑复杂(比如要跳过 null、或按多条件优先级选),FIRST_VALUE() 很难灵活调整;而 ROW_NUMBER() 配合 WHERE rn = 1 更直观、调试更方便。
示例:取每组按 score DESC 排名第一且 status = 'active' 的记录:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY dept_id
ORDER BY CASE WHEN status = 'active' THEN 0 ELSE 1 END, score DESC, id
) AS rn
FROM users
) t WHERE rn = 1;-
ROW_NUMBER()不受窗口帧影响,语义稳定 - 排序表达式里可以用
CASE实现业务优先级,FIRST_VALUE()做不到这点 - 注意:如果允许并列第一,改用
RANK()或DENSE_RANK(),但记得处理多行返回
PostgreSQL / MySQL 中的兼容性细节
MySQL 8.0 对 FIRST_VALUE() 支持完整,但早期版本(如 5.7)完全不支持窗口函数;PostgreSQL 从 9.4 开始支持,但默认帧行为和 MySQL 一致——都需手动指定 ROWS 才能覆盖整组。
- PostgreSQL 允许省略
ORDER BY(此时按物理存储顺序),但结果不可靠;MySQL 强制要求ORDER BY - 如果用在视图或物化 CTE 中,确保目标数据库版本明确支持,否则部署时直接报错
FUNCTION FIRST_VALUE does not exist - Oracle 用户注意:
FIRST_VALUE()行为类似,但默认帧是GROUPS模式,语义略有不同,迁移时要验逻辑
实际用的时候,最容易被忽略的是窗口帧范围和排序键的稳定性——哪怕只差一个 NULLS LAST 或少写一个 ROWS,结果就可能错得毫无征兆。










