SQL 补齐缺失日期后,怎么向前填充最近有效值?
本文目录
面试怎么回答
我会先区分缺日期和缺值。先生成实体与日期的有效骨架,再关联到一天一行的源数据;然后按时间累计非空次数,把最近一次有效值到下一次更新前划成一段,段内取值即可。历史最大值不等于最近值。还要处理首段无历史、展示区间前的种子值,以及零值和失效状态,不能无条件向前填。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
报价已经从 100 降到 80,报表却在第二天又显示成了 100。日期补齐了,SQL 也没有报错,问题出在一句看起来很顺手的写法:对历史报价取 MAX,就当作向前填充。
补日期解决缺行,分组填充解决缺值;最近有效值按时间决定,不是历史最大值。
先问一句:这个值能不能沿用?
向前填充不是所有指标的通用清洗方式。合同价格、设备状态等“发生变化才记录”的数据,可能适合沿用最近值;每天新增订单数,则不能把昨天的数量搬到今天。
还要区分三件事:当天没有记录、当天记录的值未知、当天真实值为零。前两种不一定允许相同处理,零更不能被当成空值跳过。以下示例约定:NULL 表示没有新的有效观测,允许沿用之前的值。
第一步,先把需要展示的日期补出来
准备实体集合和日期维表,在实体有效期内生成“实体 × 日期”的骨架,再 LEFT JOIN 观测数据。如果源表一天有多条记录,要先按业务生效时间和稳定的版本顺序选出当天最终值。
否则,补出来的不是一天一行,而是重复日期。同一天的多条数据会让窗口排序和后续填充值变得不确定。按日期取 MAX 去压重,同样会把“最新”误写成“最大”。
骨架还要有范围:只生成需要展示的实体和日期,不要把所有历史用户与几十年日期全量笛卡尔积。最基本的验收是同一实体、同一天只有一行,且日期没有断档。
第二步,按非空观测划分填充段
下面是适用于 PostgreSQL 17 的窗口写法。假设 daily 已经补齐日期,主键为 entity_id、dt,value 为数值列:
WITH marked AS (
SELECT entity_id, dt, value,
COUNT(value) OVER (
PARTITION BY entity_id
ORDER BY dt
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS fill_group
FROM daily
)
SELECT entity_id, dt, value,
MAX(value) OVER (
PARTITION BY entity_id, fill_group
) AS filled_value
FROM marked
ORDER BY entity_id, dt;
COUNT(value) 只累计非空观测。每遇到一个新值,分组号加一;后面的空值继续留在同一组里。这样每个非零分组只有一个非空值,段内 MAX 只是把它传给该段各行,不是在寻找历史最高价。
例如输入是 NULL、100、NULL、80、NULL,分组号就是 0、1、1、2、2,输出为 NULL、100、100、80、80。开头没有任何历史依据的空值,仍保持为空。

LAST_VALUE 为什么也可能不对?
LAST_VALUE 看的是窗口帧最后一行,不天然等于“最后一个非空值”。还要确认引擎是否支持 IGNORE NULLS,以及窗口是否只到当前行。PostgreSQL 17 不支持这个忽略空值选项,默认遵循保留空值的语义。
如果把窗口扩到分区末尾,又可能让后面的价格提前填到过去。面试里不能只报一个函数名,要把排序、空值规则和帧边界一起讲清楚。
一个典型场景:只有降价之后才出错
比如价格看板用运行 MAX 填补无变更日,报价上涨时结果完全正常。一旦出现降价,之后的空白日期又被填回旧高价。最初可能怀疑降价同步延迟,但检查源表发现新价格已经到达。
根因是把数值大小当作时间先后。原来的测试样本只有上涨和连续空值,没有覆盖下降,所以两种算法看起来一样。修复后应加入降价、合法零、连续多天空值和首日无历史等反例,而不是只重新跑一次正常样本。
展示区间之前的值,不能忘
报表从 9 月 1 日展示,不代表计算也只能从这一天开始。若最后一次有效报价在 8 月 30 日,要先读取区间前最近的一条有效观测作为种子,再计算并裁剪展示范围。
如果业务有“明确失效”事件,不能把失效也当作普通 NULL,否则算法会继续沿用旧值。应增加有效状态或分段重置条件,并保留原始值、填充值及来源日期,让下游知道数据是观测得到还是规则推导。