在MongoDB聚合管道处理时间序列或分组数据时,文档字段缺失和null值会直接影响统计结果与图表展示。比如物联网设备每隔一定时间上报读数,偶尔因为网络抖动没有写入新文档,或者写入时某个字段为null,后续做趋势分析就会出现断点。MongoDB在5.3版本引入了$fill阶段,专门用于在聚合管道内部填充这些空值,让数据在排序后形成更完整的序列。与简单的条件赋值不同,$fill支持按分区、按排序方向以及线性插值等方式补齐数据,更适合分析场景下的缺失值处理。

一、$fill阶段的基本语法与参数
$fill是聚合管道中的一个阶段,它不能单独用于update或findAndModify等写操作,只能出现在aggregate的数组参数中。一个典型的$fill阶段包含sortBy、partitionBy和output三个核心参数。sortBy用于指定填充时文档的排序依据,因为locf这类向前填充策略需要先确定文档的先后顺序。partitionBy是可选的,如果指定了某个字段或表达式,填充会在每个分区内独立进行,避免不同设备或不同用户的数据互相干扰。output则声明哪些字段需要填充,以及采用哪种填充方法。
下面是一个最基本的用法,对传感器温度字段做locf填充,并按时间戳升序处理:
db.sensor.aggregate([
{
$fill: {
sortBy: { ts: 1 },
output: {
temperature: { method: "locf" }
}
}
}
])
如果希望把空值替换成一个固定值,可以在output中直接指定value,而不使用method。例如把湿度字段的空值统一填成50.0,写法就是humidity: { value: 50.0 }。需要注意的是,value和method在同一字段的填充配置中只能二选一,同时出现会被解析器拒绝。
二、locf与linear两种填充策略的区别
MongoDB的$fill目前原生支持两种填充方法。locf是last observation carried forward的缩写,意思是遇到空值时使用最近一个非空值继续向后填充,直到出现新的非空值为止。这种方法适合温度、水位、在线状态等短时间内变化较慢的字段,能够保持数据的连续性,但填充出的数值不会产生渐变,而是直接复制前值。
linear则是线性插值,它要求字段是数值类型。填充某个空值时,MongoDB会找到该位置前后最近的两个非空值,根据时间或排序字段的距离计算出一个中间值。对于业务上呈现出线性变化趋势的数据,比如匀速上升的计数器、逐渐消耗的电量,linear比locf更接近真实值。例如下面一组数据:
// 原始文档
{ ts: 1, value: 10 }
{ ts: 2, value: null }
{ ts: 3, value: 30 }
如果按ts升序并使用linear填充,第二条文档的value会变成20;如果使用locf填充,它会变成10。两者的差异很明显:locf只关心前一个值,linear会综合考虑前后两个非空值。实际选择哪种方法,取决于字段本身的性质。库存数量、累计请求量这类单调递增的数值用linear可能不合适,因为线性插值会产生实际不存在的中间库存;而温度、湿度等连续物理量则更适合linear。
此外,partitionBy配合两种方法使用时也非常重要。比如要对多台设备的采集数据分别填充,必须按deviceId分区,否则第一台设备的最后读数会被错误地带到第二台设备的开头。示例写法如下:
db.metrics.aggregate([
{
$fill: {
partitionBy: "deviceId",
sortBy: { ts: 1 },
output: {
value: { method: "locf" }
}
}
}
])
这样每个deviceId内部独立执行locf,不会出现跨设备填充。
三、结合$densify补齐时间序列缺口
很多场景里的空值并不是字段为null,而是整条文档缺失。比如传感器计划每分钟上报一次,但某分钟完全没有写入任何数据,聚合管道中根本不存在对应的时间戳文档。$fill只能填充现有文档中的空字段,无法凭空创建缺失的文档。要补齐这种时间序列缺口,需要先使用$densify阶段生成缺失的时间点。
MongoDB从5.3版本开始同时提供了$densify阶段,它可以根据指定字段和步长生成新的文档,把不连续的时间序列变得连续。常见的做法是先按时间字段执行$densify,让每个时间点都有对应文档,新生成的文档中其他字段默认为null,然后再用$fill填充这些null值。下面是一个完整的示例,假设传感器每分钟上报一次:
db.sensor.aggregate([
{
$densify: {
field: "ts",
range: {
step: 1,
unit: "minute",
bounds: "full"
}
}
},
{
$fill: {
sortBy: { ts: 1 },
output: {
temperature: { method: "locf" }
}
}
}
])
这段管道执行后,原本缺失分钟对应的时间戳会被创建出来,temperature字段通过locf从前一个非空文档复制值,整条时间线就连续了。如果业务需要更精确的估计,也可以把temperature的填充方法换成linear。需要注意的是,$densify的range参数中,bounds可以是full或partition。当文档存在多个分区时,使用partition可以避免不同设备之间的时间范围被错误并集。
实际使用中还要注意$densify和$fill的执行顺序。如果先执行$fill再执行$densify,新生成的文档不会被填充,因为填充已经完成。因此两者的顺序不能颠倒,必须先补文档、再填值。
四、$fill与$set、$ifNull的对比及使用限制
在$fill出现之前,开发者通常用$set配合$ifNull或$cond来处理空值。比如要把temperature为null的文档改成默认值0,可以写{ $set: { temperature: { $ifNull: [ "$temperature", 0 ] } } }。这种方式的优点是简单直接,缺点是无法感知文档顺序,也无法做前后向填充或线性插值。如果只是把空值替换成固定默认值,$set加$ifNull完全够用;但如果需要根据相邻文档推算,就必须使用$fill或者更底层的$setWindowFields。
与$setWindowFields相比,$fill的语法更简洁,也更专注缺失值填充。$setWindowFields虽然也能通过窗口函数实现类似locf和线性插值效果,但需要编写更复杂的窗口表达式。$fill把常用策略封装成method参数,降低了使用门槛。不过这种简化也带来一些限制。当前版本中$fill的method只支持locf和linear,没有内置向后填充(next observation carried backward)选项。如果业务要求用后面的非空值填充前面的空值,可以先把排序方向反过来,执行locf,然后再排回原顺序。示例写法如下:
db.sensor.aggregate([
{ $sort: { ts: -1 } },
{
$fill: {
sortBy: { ts: -1 },
output: {
temperature: { method: "locf" }
}
}
},
{ $sort: { ts: 1 } }
])
另外,$fill作为聚合阶段,只能用于aggregate管道,不能直接修改持久化数据。如果希望将填充结果写回集合,可以在管道后面接$merge或$out阶段,但要注意目标集合的写入权限和性能开销。数据量较大时,$fill需要配合索引排序,否则会触发内存排序,可以通过allowDiskUse: true让MongoDB在磁盘上处理大数据集。
总的来说,$fill是聚合管道里处理空值的重要工具,它和$densify配合可以解决时间序列数据中的大部分连续性需求。掌握locf、linear以及partitionBy的适用场景,能够避免盲目填充带来的数据失真。
MongoDB聚合管道$fill填充空值修改时间:2026-10-01 19:18:28