导读:本期聚焦于三上悠亚创作的《SQL滑动窗口如何正确填充空值?LAST_VALUE与IGNORE NULLS详解》,敬请观看详情。时间序列或连续数据报表里的NULL值常常打断趋势分析,单纯使用LAST_VALUE很可能返回NULL或只取到当前行,达不到向前填充效果。这篇文章从默认窗口帧的机制入手,说明LAST_VALUE为什么必须配合ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW才能稳定取到最近非空值,并对比IGNORE NULLS与RESPECT NULLS的差异。同时给出COALESCE、自连接等替代方案,帮助读者在不同数据库中实现按分区、按排序键的空值填充。

处理传感器采集、用户行为、财务流水等连续数据时,NULL值几乎无法避免。设备掉线、日志丢失、字段未记录都会造成缺口。为了让报表曲线连续、计算移动平均时不被NULL打断,常见思路是把空值替换为它之前最近的一个非空值,也就是向前填充。SQL标准窗口函数中的LAST_VALUE再加上IGNORE NULLS可以比较直接地实现这个需求,但前提是必须理解窗口帧的默认行为。否则会发现函数返回的结果总是当前行的值,或者一旦遇到NULL就失效。

SQL滑动窗口如何正确填充空值?LAST_VALUE与IGNORE NULLS详解

接下来从默认窗口帧、IGNORE NULLS语法、替代实现和跨数据库兼容性几个角度展开。

一、默认窗口帧下LAST_VALUE为什么不像预期

在窗口函数中,LAST_VALUE(expr) OVER (...) 的作用是取出窗口帧内最后一行中expr的值。这里的窗口帧不是肉眼看到的分区全部行,而是根据ORDER BY和frame clause动态确定的一段范围。SQL标准规定:当OVER子句中包含ORDER BY但没有显式写ROWS或RANGE时,默认帧为RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。这个范围从分区起点一直延伸到当前行,并且包含所有与当前行排序键相等的行。

例如按时间序列排序时,如果每一行的排序键都不同,那么到当前行时,窗口帧恰好只包含当前行自己。结果就是LAST_VALUE(reading)返回当前行的reading,而不是整个分区最后一个值。如果当前行reading为NULL,函数就返回NULL。很多开发者误以为LAST_VALUE自然会返回分区最后一行,于是发现结果等于原字段值,感觉窗口函数没有生效。

SELECT
  sensor_id,
  ts,
  reading,
  LAST_VALUE(reading) OVER (
    PARTITION BY sensor_id
    ORDER BY ts
  ) AS last_value_default
FROM sensor_data
ORDER BY sensor_id, ts;

上面的查询没有给出frame clause,因此每个分区内到当前行为止的最后一行就是当前行自身。即使读取到历史数据,当前行后面还有数据,也不会被纳入计算。这个机制与SUM、AVG等累计函数一致,但对于LAST_VALUE来说尤其容易造成理解偏差。

如果希望取出整个分区的最后一个非空值,需要把窗口帧扩大为ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING;如果希望只使用当前行之前的数据做向前填充,则要指定ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。两者语义不同,前者会使用未来数据,后者不会。

二、IGNORE NULLS如何让LAST_VALUE跳过空值

即使显式设置了正确的窗口帧,LAST_VALUE(reading)在遇到NULL时仍会返回NULL,因为它不会自动忽略空值。SQL标准为FIRST_VALUE、LAST_VALUE、LEAD、LAG等窗口函数提供了IGNORE NULLS和RESPECT NULLS两个修饰词。默认是RESPECT NULLS,即NULL也是有效输入,会被函数正常返回。使用IGNORE NULLS后,函数会在窗口帧内排除值为NULL的行,只从非空行中取最后一行的值。

向前填充空值的典型写法如下:

SELECT
  sensor_id,
  ts,
  reading,
  LAST_VALUE(reading IGNORE NULLS) OVER (
    PARTITION BY sensor_id
    ORDER BY ts
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS filled_reading
FROM sensor_data
ORDER BY sensor_id, ts;

这里的窗口帧始终从分区起点到当前行。对于任意第N行,函数只观察前N行。IGNORE NULLS会过滤掉中间的空值,因此返回的是当前行之前最近的一个非空读数。如果当前行本身非空,就返回当前行;如果当前行为空,就返回再往前最后一个非空值。这就是常见的前向填充逻辑。

需要特别注意的是,不要顺手把窗口帧写成ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。那样虽然也会跳过NULL,但LAST_VALUE返回的是整个分区最后一个非空值,相当于把未来数据填入历史空值。例如月初缺数时,如果用整月最后一个非空值填充月初,就会造成数据污染。因此向前填充必须把窗口帧限制到当前行。

不同数据库对IGNORE NULLS的支持并不完全一致。Oracle较早支持,PostgreSQL从11版本开始对FIRST_VALUE、LAST_VALUE、NTH_VALUE等支持IGNORE NULLS,SQL Server 2022在LAG、LEAD、FIRST_VALUE、LAST_VALUE中也增加了该能力。MySQL 8的窗口函数目前默认遵循RESPECT NULLS,不支持显式IGNORE NULLS。因此使用前需要确认目标数据库版本。

三、不支持IGNORE NULLS时的替代实现方案

如果数据库暂时不支持IGNORE NULLS,或者项目要求兼容多种数据库,可以使用分组技巧等方案模拟前向填充。思路是给每个从非空值开始、包含后续NULL的连续区间打上相同分组号,然后在组内取唯一的非空值。

构建分组号的方式如下:对按时间排序的每一行,计算从分区起点到当前行为止非空读数的数量。每当遇到一个新的非空值,计数就加1;后续的NULL不会改变计数。这样每条NULL会继承最近一次非空值所在的分组号。

SELECT
  ts,
  reading,
  SUM(CASE WHEN reading IS NOT NULL THEN 1 ELSE 0 END)
    OVER (
      ORDER BY ts
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS grp
FROM sensor_data
ORDER BY ts;

得到grp后,再使用MAX或FIRST_VALUE在每组内填充。因为每个grp中只有一个非空值,其他都是NULL,MAX会忽略NULL,结果自然就是该组唯一的非空值。外层查询如下:

SELECT
  ts,
  reading,
  MAX(reading) OVER (PARTITION BY grp) AS filled_reading
FROM (
  SELECT
    ts,
    reading,
    SUM(CASE WHEN reading IS NOT NULL THEN 1 ELSE 0 END)
      OVER (
        ORDER BY ts
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
      ) AS grp
  FROM sensor_data
) t
ORDER BY ts;

这种写法不依赖IGNORE NULLS,通用性更强。它同样只向前看,不会引入未来数据。缺点是代码层级多一层,数据量大时窗口排序和分组需要额外计算。如果只需要在展示层处理少量数据,也可以在应用代码中迭代填充,但对于数据库内直接计算来说,上述SQL方案更利于保持查询语义一致。

另一个常见直觉是使用COALESCE配合子查询,但需要小心性能。例如每一行都执行一个相关子查询查找最近的过去非空值,通常会退化为O(n²)扫描,在百万行数据上几乎不可用。窗口函数方案能够在一次排序和扫描内完成计算,实际性能通常远好于逐行子查询。

四、排序键、分区键与NULL语义的工程细节

在实际生产环境中,孤立地写好函数语法还不够,排序键和分区键的设计直接影响填充结果。时间戳是最常见的排序键,但如果同一秒出现多条记录,RANGE和ROWS会有差异。RANGE按排序键值分组,相同排序值的行彼此可见;ROWS按物理行顺序推进。若时间戳重复且希望严格按插入顺序向前填充,使用ROWS更合适;若相同时间戳的记录在业务上不应互相影响,则可以用RANGE并增加其他排序键消除二义性。

分区键的选择同样重要。前向填充通常应该按传感器编号、用户ID或商品SKU等业务实体进行。如果忘记写PARTITION BY,窗口函数会把整张表按时间排序,可能出现设备A的空值被设备B的非空值填充。尤其是多主体数据混在一起时,这种错误不容易从结果表面发现。因此写查询前要明确填充的上下文边界。

NULL语义方面,IGNORE NULLS只作用于窗口帧内的NULL行,它不会把空字符串、零值或默认值一并处理。比如有些表用-999表示无效读数,这类值不会因为IGNORE NULLS而被跳过。此时可以在CASE表达式中先把哨兵值转换为NULL,再交给LAST_VALUE处理。类似地,若业务上需要同时填充多个字段,可以为每个字段分别写窗口函数,但要注意所有窗口函数使用相同的排序键和帧,避免出现填充结果错位。

最后,如果表中存在连续大量NULL,向前填充会得到一个很久之前的旧值。对于监控告警、库存预测等场景,旧值可能已经失去参考意义。可以在外查询中设置最大填充距离,例如只填充最近3个周期内的缺失值。这可以通过比较当前时间与最近非空时间的差值实现,也可以先生成带最近非空时间戳的中间结果,再用条件判断过滤。

SQL窗口函数LAST_VALUEIGNORE NULLS修改时间:2026-08-27 18:00:02

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。