first_value返回的不是分组第一条,因其不保证物理顺序,仅按order by定义的逻辑顺序取值;缺order by会报错或结果不确定,且默认窗口帧仅覆盖当前行及之前行,必须显式指定partition by和rows between unbounded preceding and unbounded following,并用created_at、id等稳定字段排序才能准确获取分组首行值。

FIRST_VALUE 为什么返回的不是“分组第一条”?
直接用 FIRST_VALUE 却没拿到每组第一条记录,大概率是因为没配 OVER 子句里的 ORDER BY —— 这个函数本身不保证物理顺序,它只按你指定的排序逻辑取“第一个值”。如果漏写 ORDER BY,部分数据库(如 PostgreSQL)会报错,而 SQL Server 或 Oracle 可能返回不确定结果,看着像随机。
更关键的是:即使写了 ORDER BY,FIRST_VALUE 默认窗口是 UNBOUNDED PRECEDING TO CURRENT ROW,它不会自动“按组切片”。必须显式加上 PARTITION BY 才能实现分组内计算。
- 正确写法必须同时包含
PARTITION BY和ORDER BY - 排序字段要选有业务意义的,比如
created_at、id;仅用ORDER BY id但 id 不连续或有删改,结果仍可能不符合预期 - 别依赖表的插入顺序,SQL 标准里没有“默认顺序”这回事
怎么写才能真正拿到每组第一条完整记录?
FIRST_VALUE 只返回某个字段的值,不是整行。如果你需要整条记录(比如同时拿到 name、status、amount),不能只靠一个 FIRST_VALUE 函数,得配合子查询或 CTE + 行号。
常见做法是用 ROW_NUMBER() 配合 PARTITION BY:
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at ASC) AS rn
FROM orders
) t
WHERE rn = 1;
这个比嵌套多个 FIRST_VALUE(col) 更可靠、更易读,也避免了不同字段排序逻辑不一致导致的数据错位(比如对 name 按字母排、对 amount 按大小排,就完全不是同一条记录了)。
- 用
ROW_NUMBER()是获取“第一条完整记录”的事实标准做法 -
FIRST_VALUE更适合“把分组首条的某个字段复制到本组所有行”,比如打上首单时间标签 - 如果真要用
FIRST_VALUE做多字段,得为每个字段单独写一次,且ORDER BY必须一致,否则语义混乱
不同数据库对 FIRST_VALUE 的兼容性要注意什么?
MySQL 8.0+、PostgreSQL 8.4+、SQL Server 2005+、Oracle 200i+ 都支持 FIRST_VALUE,但细节差异不小:
- MySQL 8.0 之前不支持窗口函数,强行用会报错
ERROR 1064 - PostgreSQL 要求
ORDER BY必填,不写直接报错;SQL Server 允许省略但行为未定义 - SQLite 从 3.25.0 开始支持,但不支持
RANGE窗口框架,只认ROWS - 如果跨数据库迁移,别假设
OVER (PARTITION BY x)不带ORDER BY能跑通
性能和索引怎么配合?
FIRST_VALUE 本身不慢,慢的是底层排序。当数据量大时,OVER (PARTITION BY a ORDER BY b) 会触发对每个分组做局部排序——如果没有对应索引,就会走全表扫描 + 内存排序,I/O 和 CPU 开销明显上升。
最有效的索引是复合索引:(a, b),顺序不能颠倒。例如:
CREATE INDEX idx_category_created ON orders (category, created_at);
- 索引
(category, created_at)能让分组 + 排序一步到位,避免额外排序操作 - 只建
(created_at)没用,因为PARTITION BY字段不在索引前列 - 如果查询还带
WHERE条件,比如WHERE status = 'active',考虑把该字段也加入索引前缀,但注意索引宽度和维护成本
窗口函数容易被当成“语法糖”忽略执行计划,实际上线前务必看 EXPLAIN 或执行计划里的 Sort / WindowAgg 节点开销。










