SQL数据缺失值线性插值:告别生硬填充,实现平滑估算
处理时间序列数据时,缺失值是个绕不开的麻烦。直接留空影响分析,用上一个值简单填充又显得过于生硬。这时候,线性插值就成了一个更优雅的选择——它能在已知数据点之间,估算出一条合理的“连线”。但问题是,在SQL里怎么实现这个听起来有点“数学”的操作?
关键在于,你得先找到缺失值前后最近的两个有效数据点,然后按距离分配权重。这可不是一个简单的LAG()或LEAD()就能搞定的。
LAG()/LEAD()仅取相邻非空值,无法跳过连续空值找最近有效点;线性插值需前后最近非空值及其位置,通过LAST_VALUE/ FIRST_VALUE IGNORE NULLS与时间戳提取实现加权计算。

为什么不能直接用 LAG() 和 LEAD() 做线性插值?
这里有个常见的误区。很多人第一反应就是用LAG(col)和LEAD(col)去取前后值。但仔细一想,这方法行不通。为什么呢?因为这两个函数非常“老实”,它们只认物理上紧挨着的前一行或后一行。如果当前行是空值,它的前一行碰巧也是空值,那么LAG()返回的依然是NULL,它不会聪明地跳过连续的空值,去找到更早的那个有效数据。
而线性插值的核心,需要的是“前一个非空值的位置和值”以及“后一个非空值的位置和值”。你必须得能“穿透”中间所有的空值,定位到真正有效的锚点才行。
用窗口函数定位前后最近非空值的行号和值
那么,正确的打开方式是什么?答案是:窗口函数。核心思路是进行两次窗口聚合——一次正向累积寻找前驱,一次反向累积寻找后继。
这里的关键不在于直接拿到值,而在于同时锚定值及其对应的位置(比如时间戳或行号)。具体怎么操作呢?
- 在PostgreSQL 14及以上版本,语法比较直观:可以使用
MAX(val) FILTER (WHERE val IS NOT NULL) OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING)。这能获取“截至当前行,最近的一个前向非空值”。 - 如果是在MySQL 8.0+或SQL Server等不支持
FILTER子句的数据库里,可以改用LAST_VALUE(val) IGNORE NULLS OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING)。这个IGNORE NULLS子句就是跳过空值的神器。 - 找后一个非空值同理,把方向反过来:
FIRST_VALUE(val) IGNORE NULLS OVER (ORDER BY ts ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)。 - 光有值还不够,必须同步获取这两个锚点对应的时间戳。建议用类似的窗口逻辑,比如
MAX(ts) FILTER (WHERE val IS NOT NULL) OVER (...)来提取时间。
算出插值系数并完成线性计算
好了,现在“弹药”齐了:前值prev_val、后值next_val、前时间prev_ts、后时间next_ts,以及当前时间curr_ts。剩下的就是一道经典的数学题了。
插值公式很简单:(next_val - prev_val) * (curr_ts - prev_ts) / (next_ts - prev_ts) + prev_val。说白了,就是按时间距离的比例,在前值和后值之间进行加权平均。
这里有个细节必须注意:除零保护。如果prev_ts和next_ts相等(比如所有时间戳都相同,或者前后锚点意外重合),分母为零会导致计算错误。这时候,直接取prev_val就行。
- 在MySQL中,可以用
COALESCE(..., prev_val)包裹整个计算式,当内部结果为NULL(即除零发生时)时回退到前值。 - PostgreSQL里更优雅一些,可以用
NULLIF(next_ts - prev_ts, 0)先把零值分母转为NULL,再利用NULL参与运算结果为NULL的特性,最后用COALESCE处理。 - 另外,如果时间列是日期时间类型,记得先统一单位,比如用
EXTRACT(EPOCH FROM ...)转换成秒数,这样相减才能得到数值差。
完整可运行示例(PostgreSQL)
SELECT
ts,
val,
COALESCE(
val,
(
(next_val - prev_val) * (EXTRACT(EPOCH FROM ts) - prev_epoch)
/ NULLIF(next_epoch - prev_epoch, 0)
+ prev_val
)::NUMERIC(10,3)
) AS val_interp
FROM (
SELECT
ts,
val,
LAST_VALUE(val) IGNORE NULLS OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING) AS prev_val,
FIRST_VALUE(val) IGNORE NULLS OVER (ORDER BY ts ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS next_val,
MAX(CASE WHEN val IS NOT NULL THEN EXTRACT(EPOCH FROM ts) END)
OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING) AS prev_epoch,
MIN(CASE WHEN val IS NOT NULL THEN EXTRACT(EPOCH FROM ts) END)
OVER (ORDER BY ts ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS next_epoch
FROM samples
) t;
最后需要提醒一点:线性插值有个天然的前提,就是缺失的这段数据,两端必须都有非空值。如果序列开头就是空值,找不到前驱;或者末尾是空值,找不到后继,那么插值结果自然还是NULL。这不是SQL写法的问题,而是方法本身的逻辑限制。理解这一点,才能更好地应用这个工具。
