first_value默认不返回分组第一行,因其依赖order by定义逻辑顺序且默认窗口帧为rows between unbounded preceding and current row;必须显式指定order by(如created_at asc)和完整窗口帧(rows between unbounded preceding and unbounded following)才能准确获取分组首行特征。

为什么FIRST_VALUE返回的不是分组第一行数据
常见现象是:明明写了 PARTITION BY,但 FIRST_VALUE(col) 结果和原始顺序不一致,甚至每组都返回同一值。根本原因是 FIRST_VALUE 默认按当前窗口帧(ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)计算,不指定排序时行为不可控,数据库可能按物理存储顺序或优化器选择的任意顺序取“第一个”。必须显式用 ORDER BY 定义逻辑顺序。
- 没写
ORDER BY的FIRST_VALUE是无效实践,结果无意义 -
ORDER BY必须作用于窗口定义中,不能只写在外部ORDER BY - 若需按时间戳取最早记录,
ORDER BY created_at ASC;若按ID取最小主键,则ORDER BY id ASC
如何用FIRST_VALUE配合WHERE筛选出真正的首行记录
单纯用 FIRST_VALUE 只能生成一列值,无法直接过滤整行。常见错误是写成 WHERE col = FIRST_VALUE(col) OVER (...) —— 这会把所有行都保留(因为每行窗口计算都含该组首值)。正确做法是用子查询或 CTE 先算出首值,再关联原表匹配。
- 推荐方式:用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col)更直观,WHERE rn = 1直接得首行 - 硬要用
FIRST_VALUE:先在子查询中计算FIRST_VALUE(id) OVER (PARTITION BY type ORDER BY updated_at)得到每组首个id,再JOIN原表查完整记录 - 注意:若
sort_col有重复值,FIRST_VALUE返回其中一个(不确定是哪条),而ROW_NUMBER()能稳定排序(靠第二排序字段或id)
MySQL 8.0+ 和 PostgreSQL 中 FIRST_VALUE 的行为差异
两者语法一致,但默认窗口帧处理略有不同。MySQL 8.0 对未声明 ROWS 或 RANGE 的窗口函数,默认使用 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW;PostgreSQL 则对未指定帧的聚合类窗口函数(如 AVG)用 RANGE,但 FIRST_VALUE 属于取值类,仍需显式 ORDER BY 才有效。
- MySQL 中漏写
ORDER BY可能报错或返回警告,PostgreSQL 通常静默接受但结果随机 - PostgreSQL 支持
IGNORE NULLS(如FIRST_VALUE(val IGNORE NULLS)),MySQL 8.0.22+ 才开始支持 - 性能上,两者都依赖排序操作,大数据量时
PARTITION BY + ORDER BY字段必须有复合索引(如(group_col, sort_col))
替代方案:比FIRST_VALUE更稳妥的“取首行”写法
多数场景下,FIRST_VALUE 并非最优解。它本质是“复制首行某列值到所有行”,而非“取出首行”。真正要提取整行记录,ROW_NUMBER() 是最通用、可读性最强的方式。
- 写法简洁:
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn,外层WHERE rn = 1 - 可处理并列情况:改用
RANK()或DENSE_RANK()保留并列首名 - 若只要某列值且不关心整行,
FIRST_VALUE没问题;但一旦涉及多字段、去重、或后续 JOIN,立刻转向ROW_NUMBER() - 特别注意:SQL 标准里没有“分组首行”的原子操作,所有方案都依赖排序定义——这个前提容易被忽略,却决定结果是否可靠











