FIRST_VALUE不能直接填补空缺值,因为它仅返回窗口内排序后首个值(默认不忽略NULL),不修改原数据、不生成新行、也不自动向前填充;必须显式使用IGNORE NULLS、稳定排序(如ORDER BY ts, id)及正确窗口帧(ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)才能实现“取最近非空值”。

为什么FIRST_VALUE不能直接填补空缺值
FIRST_VALUE 是窗口函数,它只返回窗口内排序后第一个非 NULL 值(取决于 IGNORE NULLS 设置),但它本身不修改原数据、不生成新行、也不“向前填充”到当前行为 NULL 的位置——它只是按规则取一个值。如果你写 SELECT FIRST_VALUE(col) OVER (ORDER BY ts),而 col 在某行是 NULL,那这行的 FIRST_VALUE 结果仍是 NULL(默认不忽略 NULL),除非你显式加 IGNORE NULLS,且该窗口内存在更早的非 NULL 值。
常见误用现象:
– 查询结果里仍有大量 NULL,以为函数没生效
– 用 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 却没加 IGNORE NULLS,导致取到的是当前行(即 NULL)
– 没按时间戳严格排序,或排序字段含重复值未加唯一键,导致窗口顺序不可靠
正确写法:用FIRST_VALUE + IGNORE NULLS + 稳定排序
真正能实现“向前取最近非空值”的写法,必须同时满足三个条件:
-
FIRST_VALUE(col) IGNORE NULLS OVER (ORDER BY ts, id)——IGNORE NULLS是关键开关,缺了它就无效 -
ts必须是严格递增的时间列;若有并行事件,务必补一个唯一排序辅助字段(如自增id或row_number()),否则窗口可能随机取值 - 窗口帧建议显式写成
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,避免数据库默认帧(有些引擎默认是RANGE,对时间戳易出错)
示例(PostgreSQL / Oracle / BigQuery 支持):
SELECT
ts,
val,
FIRST_VALUE(val) IGNORE NULLS OVER (
ORDER BY ts, id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS filled_val
FROM sensor_data;
注意:MySQL 8.0+ 也支持 IGNORE NULLS,但 SQL Server 和旧版 PostgreSQL 不支持,需换用 LAG 递归或子查询模拟。
替代方案:当数据库不支持IGNORE NULLS时怎么办
在 SQL Server 或 PostgreSQL < 14 中,FIRST_VALUE ... IGNORE NULLS 会报错。此时不能硬套语法,得绕路:
- 用
MAX(val) OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING)—— 仅适用于val是单调不减的场景(比如版本号),否则会取到“历史最大值”而非“最近非空值” - 用关联子查询:
(SELECT TOP 1 val FROM t2 WHERE t2.ts <= t1.ts AND t2.val IS NOT NULL ORDER BY t2.ts DESC),性能差,大数据量慎用 - 用
LAG多层嵌套(最多填 N 层空):不通用,维护成本高 - 更可靠的做法:先用
ROW_NUMBER() OVER (PARTITION BY grp ORDER BY ts)对连续 NULL 分组,再用MAX(val) OVER (PARTITION BY grp)—— 这需要先用LAG或递归 CTE 构造分组标识grp
性能与边界情况提醒
即使语法正确,实际使用中几个容易被跳过的点:
- 时间戳字段没索引 →
ORDER BY ts窗口排序变全表扫描,千万级表可能秒变分钟级 - 空缺集中在头部(前几行都是 NULL)→
IGNORE NULLS仍返回 NULL,因为窗口内无可用非空值;需额外用COALESCE(..., <em>default</em>)补底 - 时序数据跨天/跨月聚合后重排 → 若原始
ts被截断(如只留日期),排序失去精度,必须保留原始毫秒级时间戳参与ORDER BY -
FIRST_VALUE是确定性函数,但结果依赖窗口定义和 NULL 处理策略,不同数据库对IGNORE NULLS的实现细节略有差异(例如 Oracle 允许在RANGE帧中用,BigQuery 要求ROWS)
真正难的不是写对一行 SQL,而是确认你的时序数据是否真的“有序可比”,以及目标数据库是否在那个版本里悄悄改了窗口函数的 NULL 行为。

















