first_value本身不跳过null,仅按排序取首行值;需配合coalesce、filter(where col is not null)或case排序将非null值前置,才能实现有效填充。

FIRST_VALUE 本身不能直接“填充空值”,它只是窗口函数,返回分区中第一行的某个字段值;想用它来补空,必须配合 COALESCE 或条件逻辑,且要确保窗口定义能覆盖到空值所在行的“有效首值”。
为什么直接用 FIRST_VALUE(...) OVER (...) 无法填空
因为 FIRST_VALUE 按照你定义的 ORDER BY 和 PARTITION BY 计算——如果某行的值本身就是 NULL,而你又没做任何空值规避,它可能取到 NULL(比如首行就是空),或者根本没把非空值“拉”到当前行上下文中。
-
FIRST_VALUE不跳过NULL:默认按排序取第一行,不管那行值是不是NULL - 窗口范围默认是
UNBOUNDED PRECEDING TO CURRENT ROW,但这个范围不改变“哪一行算 first”——只由ORDER BY决定 - 若想让空值行拿到“最近的上方非空值”,得用
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW+ 排序策略,但更稳妥的是先过滤/偏移非空行
正确做法:用 FIRST_VALUE 配合非空子集 + 稳定排序
核心思路:构造一个只含非空值的逻辑序列,再让每个空值行去“找”这个序列里排在它前面(或同一组内)的第一个非空值。常用组合是:FIRST_VALUE(col) OVER (PARTITION BY grp ORDER BY order_key),其中 grp 是你定义的业务分组(如用户 ID),order_key 要保证非空值优先、且顺序可预测。
- 先用
CASE WHEN col IS NOT NULL THEN order_col END做排序键,让非空值排前面,NULL被排到末尾(或用NULLS LAST) - 或者用
ROW_NUMBER() OVER (PARTITION BY id ORDER BY CASE WHEN col IS NOT NULL THEN 0 ELSE 1 END, ts)生成带优先级的序号,再用它当ORDER BY字段 - 示例(按时间向前填充):
SELECT id, ts, val, COALESCE(val, FIRST_VALUE(val) FILTER (WHERE val IS NOT NULL) OVER (PARTITION BY id ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS filled_val FROM your_table;注意这里用了FILTER子句(PG 9.4+),显式只从非空行中取 first
比 FIRST_VALUE 更适合“向前填充”的替代方案
如果目标是典型的“用上一个非空值覆盖当前空值”(即 LOCF:Last Observation Carried Forward),FIRST_VALUE 其实是绕路的。更直接的是:
- 用
LAG+ 递归 CTE 或窗口聚合模拟状态传递——但复杂 - 用
MAX(val) FILTER (WHERE val IS NOT NULL) OVER (PARTITION BY id ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW):利用MAX天然忽略NULL的特性,效果等价于“到目前为止见过的最后一个非空值” - PostgreSQL 14+ 可用
IGNORE NULLS(仅限LAG/LEAD):LAG(val) IGNORE NULLS OVER (PARTITION BY id ORDER BY ts),这才是最贴近语义的写法
容易被忽略的边界情况
真正上线时出错往往不在语法,而在数据分布和窗口边界:
- 分组字段(
PARTITION BY)漏写或写错,导致跨业务线污染——比如用户 A 的非空值被用来填用户 B 的空值 -
ORDER BY字段存在重复值且未加唯一锚点(如主键),会导致窗口排序不稳定,FIRST_VALUE结果不可预期 - 空值集中在分组开头:即使用了
FILTER,FIRST_VALUE在前几行仍会返回NULL(因为还没见到非空值),这时COALESCE也救不了——得额外处理首段缺失 - 性能隐患:对大表用多层窗口嵌套(如先算分组非空序列再 join 回原表)可能比一次扫描慢数倍,建议先
EXPLAIN ANALYZE










