传感器、行情、日志这类时间序列数据在写入过程中经常出现空值,原因可能是设备短暂离线、采样周期波动,或者数据清洗时主动移除了异常点。若直接对这些数据做移动平均、趋势计算或可视化,空值会被当成0或直接跳过,最终结果容易失真。MongoDB从5.3版本开始提供$linearFill聚合管道阶段,借助相邻非空值进行线性插值,快速补齐缺失点。它把原本需要脚本循环或复杂窗口表达式的操作简化成一个阶段,适合在数据库侧完成数据整形。

独立阶段$linearFill的语法与结果
独立使用$linearFill时,它作为聚合管道中的一个阶段出现,通常放在$sort之后。该阶段内部包含两个关键部分:sortBy和output。sortBy声明计算插值时依赖的排序字段,它必须与前面执行的排序顺序保持一致;output中列出需要填充的字段,值为1表示对该字段启用线性填充。一个阶段可以同时处理多个字段,例如温度、湿度都可以在同一阶段补齐。
下面的语法片段展示了最基础的写法。这里假设文档已经按timestamp升序排列,output中的temperature和humidity都会参与线性插值。
{
$linearFill: {
sortBy: { timestamp: 1 },
output: {
temperature: 1,
humidity: 1
}
}
}
需要注意,$linearFill本身不会改变文档的物理顺序,也不会替代$sort。如果管道中没有前置排序,或者排序字段与sortBy不一致,插值结果可能不符合预期。另外,它只对数值类型字段执行插值,布尔、字符串或日期字段即使出现空值也不会被线性填充。
先准备一批带缺失值的传感器数据
为了直观看到效果,可以先向集合中写入几台设备的温度读数。下面示例中,_id为2、4、5的文档温度字段为null,模拟设备在部分时间点没有上报数据。实际业务中,也可能直接缺失temperature字段,$linearFill对这两种情况都会处理。
db.readings.insertMany([
{ _id: 1, device: "A", timestamp: ISODate("2024-01-01T00:00:00Z"), temperature: 20 },
{ _id: 2, device: "A", timestamp: ISODate("2024-01-01T00:01:00Z"), temperature: null },
{ _id: 3, device: "A", timestamp: ISODate("2024-01-01T00:02:00Z"), temperature: 24 },
{ _id: 4, device: "A", timestamp: ISODate("2024-01-01T00:03:00Z"), temperature: null },
{ _id: 5, device: "A", timestamp: ISODate("2024-01-01T00:04:00Z"), temperature: null },
{ _id: 6, device: "A", timestamp: ISODate("2024-01-01T00:05:00Z"), temperature: 30 }
])
执行聚合时,管道先按时间升序排列,再调用$linearFill。执行后,_id为2的文档会得到22,因为20和24间隔两分钟,中间值正好是22;_id为4和5的文档分别得到26和28,因为24到30间隔三个点位,按比例依次递增2。
db.readings.aggregate([
{ $sort: { timestamp: 1 } },
{
$linearFill: {
sortBy: { timestamp: 1 },
output: {
temperature: 1
}
}
}
])
得到的结果中,原本为null的值被替换成线性插值,而不是直接照搬相邻值。这种按时间间隔均分差值的方式,更符合连续变化型数据的趋势,例如温度、压力、流量等物理量。
窗口操作符形态与独立阶段的区别
除了作为独立聚合阶段,$linearFill还可以在$setWindowFields中作为窗口操作符使用。窗口形态支持partitionBy,可以按设备或其他维度分组后再填充,避免不同设备之间的数据相互干扰。下面的示例按device分区,相当于先按设备分组,再在各组内使用线性插值。
db.readings.aggregate([
{
$setWindowFields: {
partitionBy: "$device",
sortBy: { timestamp: 1 },
output: {
temperature: {
$linearFill: "$temperature"
}
}
}
}
])
独立阶段$linearFill的优势是语法更短,适合只有线性填充需求的场景。而$setWindowFields中可以同时使用多个窗口操作符,比如既做线性填充,又计算移动平均或排名。两者在底层计算逻辑上类似,但窗口形态的灵活度更高。如果数据已经按设备分片或明确需要分区,推荐使用窗口操作符形式;如果只是单序列填充,独立阶段足够简洁。
边界空值需要搭配其他填充策略
线性插值的本质要求缺失点两侧都有参考值。如果空值出现在序列最前或最后,只存在一侧数据,那么斜率无法计算,$linearFill会保持这些位置为空。例如文档开头第一条温度就是null,后续才有正常值时,第一条不会被填充。同理,最后一条缺失时也不会被处理。
对于这种边界情况,通常需要再追加一个$fill阶段,使用locf表示用前一个非空值向后填充,或者使用constant指定固定值。下面示例先执行线性填充,再对仍然为空的字段使用前向填充,从而让首尾也能获得合理数值。
db.readings.aggregate([
{ $sort: { timestamp: 1 } },
{ $linearFill: { sortBy: { timestamp: 1 }, output: { temperature: 1 } } },
{
$fill: {
sortBy: { timestamp: 1 },
output: {
temperature: { method: "locf" }
}
}
}
])
实际使用时还应当注意排序字段的唯一性。如果多个文档具有相同的timestamp值,线性插值的参考点选择会变得不明确,容易出现不可预期的结果。同时,如果缺失值跨度很长,线性插值可能掩盖真实的数据波动,因此插值更适合短时间内的缺口,长时间缺失建议从业务侧重新采集或标记为异常。
MongoDB聚合管道线性填充$linearFill修改时间:2026-09-23 06:34:18