FIRST_VALUE不能直接填充缺失值,因其默认不跳过NULL;需用IGNORE NULLS(PostgreSQL/BigQuery)或LAST_VALUE+CASE排序(SQL Server)、ROW_NUMBER()(MySQL)等替代方案。

为什么 FIRST_VALUE 不能直接“填充缺失值”
FIRST_VALUE 是窗口函数,它只返回当前窗口帧内的第一个非 NULL 值(或按 ORDER BY 排序后的首行值),**不会自动跳过 NULL 去找下一个非空值**。如果你的维度列本身是 NULL,而你又没做预处理,FIRST_VALUE(dim_col) 很可能就返回 NULL——这不是“填充”,只是原样复读。
真正能“填充”的,是结合 IGNORE NULLS(PostgreSQL / Oracle / BigQuery 支持)或用 COALESCE + 自定义排序兜底(如 SQL Server / MySQL 8.0+)。
- PostgreSQL / BigQuery:直接写
FIRST_VALUE(dim_col) IGNORE NULLS OVER (PARTITION BY group_col ORDER BY ts) - SQL Server:不支持
IGNORE NULLS,得改用LAST_VALUE配合ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,再加COALESCE回退逻辑 - MySQL 8.0+:同样不支持
IGNORE NULLS,但可用ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY CASE WHEN dim_col IS NOT NULL THEN 0 ELSE 1 END, ts)找最近非空行
用 FIRST_VALUE + IGNORE NULLS 填充时间序列中的维度断点
典型场景:设备上报表中 device_type 只在首次注册时有值,后续记录为 NULL,你想按设备 ID 把它“拖下来”补全。
SELECT
device_id,
event_time,
FIRST_VALUE(device_type) IGNORE NULLS OVER (
PARTITION BY device_id
ORDER BY event_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS device_type_filled
FROM events;
注意三点:
- 必须显式写
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,否则默认窗口可能不包含前面所有行 -
ORDER BY event_time要确保时间有序,乱序会导致填充错位 - 如果某
device_id全程无任何非空device_type,结果仍是NULL,需额外用COALESCE(..., 'unknown')
SQL Server 中绕过 IGNORE NULLS 缺失的等效写法
SQL Server 的 FIRST_VALUE 无视 NULL 的能力不存在,但可以用 LAST_VALUE 反向模拟:把非空值“推”到后面所有行。
SELECT
device_id,
event_time,
LAST_VALUE(device_type) OVER (
PARTITION BY device_id
ORDER BY CASE WHEN device_type IS NOT NULL THEN 0 ELSE 1 END, event_time
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS device_type_filled
FROM events;
这个技巧依赖两个关键点:
-
CASE WHEN ... THEN 0 ELSE 1 END让所有非空值排在空值前面 -
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING确保窗口覆盖整个分组,否则LAST_VALUE在当前行前找不到非空值就返回NULL - 性能上比原生
IGNORE NULLS稍差,因为排序字段变复杂了
容易被忽略的边界:分区键变更与重复时间戳
填充逻辑高度依赖 PARTITION BY 和 ORDER BY 的组合。一旦出现以下情况,结果会出人意料:
- 同一
device_id下有两条记录时间戳完全相同(event_time重复),且其中一条device_type为空——FIRST_VALUE返回哪条取决于底层排序稳定性,不可靠 - 维度值中途变更(比如设备从 “mobile” 切到 “tablet”),但你的业务本意是“首次值固定”,那就该用
FIRST_VALUE;如果想“最新值延续”,就得换LAST_VALUE+ 合理窗口 - 分区键(如
device_id)本身含NULL,会导致这些行被归入同一隐式分区,互相污染填充结果——务必提前WHERE device_id IS NOT NULL
最稳妥的做法永远是:先用子查询或 CTE 显式提取每个分组的首个非空维度值,再 JOIN 回主表。窗口函数方便,但可读性和可控性不如显式聚合。










