MongoDB聚合:$densify

$densify阶段可以为文档序列中字段缺失的某些值创建新文档。其主要的用途有:

  • 补齐时间序列数据。
  • 为分组数据添加缺失值。
  • 为指定的值范围填充数据。

语法

$densify阶段的语法:

js 复制代码
{
   $densify: {
      field: <fieldName>,
      partitionByFields: [ <field 1>, <field 2> ... <field n> ],
      range: {
         step: <number>,
         unit: <time unit>,
         bounds: < "full" || "partition" > || [ < lower bound >, < upper bound > ]
      }
   }
}

$densify阶段的参数主要有以下字段:

field

必选字段,要填充的字段,字段的值必须是数值或日期类型,文档中其他没有被field指定的字段则会直接通过管道,不会被修改。如果指定的<field>在内嵌文档或数组中,需要使用点号。

partitionByFields

可选字段,一组字段构成的键值用于对文档进行分组。在$densify阶段,每组文档被称为一个分区。若省略这个字段,$densify则对整个集合使用一个分组。

range

必选字段,一个对象,指定数据如何被填充。

range.bounds

必选字段,可以用下面两种方式指定range.bounds

  • 数组形式:[ < lower bound >, < upper bound > ],
  • 字符串形式:"full""partition"

如果bounds是一个数组:

  • $densify添加涵盖指定范围值的文档。
  • bounds的数据类型必须与填充的字段类型保持一致。

如果bounds"full"

  • $densify添加涵盖指定范围值的文档。

如果bounds"partition"

  • $densify将文档添加到每个分区,类似于单独对每个分区单独运行full范围致密化。

range.step

必须字段,所有文档中field值的增量,$densify按照step(步长)在已经存在的文档之间创建新文档。如果指定了range.unit字段,step必须是一个整数,否则step可以是任意的数值。

range.unit

如果field为日期则是必须字段,unit用于日期类型的field增量的单位。可以指定为下面字符串的值之一:

  • millisecond
  • second
  • minute
  • hour
  • day
  • week
  • month
  • quarter
  • year

使用

field的限制

如果文档包含的field有下列情况将报错:

  • 集合中任何文档中指定的field值有日期类型,且没有指定unit字段的
  • 集合中任何文档中指定的field值有数值类型,且没有指定unit字段的
  • 如果要densify的字段field名称以$开头,必须要对其改名,可以使用$project

partitionByFields的限制

下面的情况出现在partitionByFields数组字段名中时将报错:

  • 求值结果为非字符串值
  • $开头

range.bounds的行为

如果range.bounds是一个数组:

  • 下限值表示新增文档的起始值,与集合中已有的文档无关。
  • 下限包含在内。
  • 上限不包含在内。
  • $densify不会过滤掉字段值超出指定范围的文档。

输出顺序

$densify不保证输出文档的顺序。想要保证有序,可以使用$sort对想要排序的字段进行排序。

举例

填充时间序列数据

创建一个包含四小时温度读数的weather集合。

js 复制代码
db.weather.insertMany( [
   {
       "metadata": { "sensorId": 5578, "type": "temperature" },
       "timestamp": ISODate("2021-05-18T00:00:00.000Z"),
       "temp": 12
   },
   {
       "metadata": { "sensorId": 5578, "type": "temperature" },
       "timestamp": ISODate("2021-05-18T04:00:00.000Z"),
       "temp": 11
   },
   {
       "metadata": { "sensorId": 5578, "type": "temperature" },
       "timestamp": ISODate("2021-05-18T08:00:00.000Z"),
       "temp": 11
   },
   {
       "metadata": { "sensorId": 5578, "type": "temperature" },
       "timestamp": ISODate("2021-05-18T12:00:00.000Z"),
       "temp": 12
   }
] )

下面使用$densify阶段来填补四小时间隔之间的空白,实现小时粒度的数据点:

js 复制代码
db.weather.aggregate( [
   {
      $densify: {
         field: "timestamp",
         range: {
            step: 1,
            unit: "hour",
            bounds:[ ISODate("2021-05-18T00:00:00.000Z"), ISODate("2021-05-18T08:00:00.000Z") ]
         }
      }
   }
] )

在这个例子中:

  • $densify阶段填充温度记录之前的空缺时间
    • field"timestamp"填充timestamp(时间戳)字段
    • range:
      • step:1表示timestamp字段的增量为1个单位
      • unit:hour表示timestamp字段填充为小时
      • bounds: [ ISODate("2021-05-18T00:00:00.000Z"), ISODate("2021-05-18T08:00:00.000Z") ]设置了要填充的时间范围

在下面的输出中,$densify阶段以小时为单位填充了00:00:0008:00:00之间的间隙:

js 复制代码
[
  {
    _id: ObjectId("618c207c63056cfad0ca4309"),
    metadata: { sensorId: 5578, type: 'temperature' },
    timestamp: ISODate("2021-05-18T00:00:00.000Z"),
    temp: 12
  },
  { timestamp: ISODate("2021-05-18T01:00:00.000Z") },
  { timestamp: ISODate("2021-05-18T02:00:00.000Z") },
  { timestamp: ISODate("2021-05-18T03:00:00.000Z") },
  {
    _id: ObjectId("618c207c63056cfad0ca430a"),
    metadata: { sensorId: 5578, type: 'temperature' },
    timestamp: ISODate("2021-05-18T04:00:00.000Z"),
    temp: 11
  },
  { timestamp: ISODate("2021-05-18T05:00:00.000Z") },
  { timestamp: ISODate("2021-05-18T06:00:00.000Z") },
  { timestamp: ISODate("2021-05-18T07:00:00.000Z") },
  {
    _id: ObjectId("618c207c63056cfad0ca430b"),
    metadata: { sensorId: 5578, type: 'temperature' },
    timestamp: ISODate("2021-05-18T08:00:00.000Z"),
    temp: 11
  }
  {
    _id: ObjectId("618c207c63056cfad0ca430c"),
    metadata: { sensorId: 5578, type: 'temperature' },
    timestamp: ISODate("2021-05-18T12:00:00.000Z"),
    temp: 12
  }
]

分区填充

创建一个coffee数据集,其中包含两种咖啡豆的数据:

js 复制代码
db.coffee.insertMany( [
   {
      "altitude": 600,
      "variety": "Arabica Typica",
      "score": 68.3
   },
   {
      "altitude": 750,
      "variety": "Arabica Typica",
      "score": 69.5
   },
   {
      "altitude": 950,
      "variety": "Arabica Typica",
      "score": 70.5
   },
   {
      "altitude": 1250,
      "variety": "Gesha",
      "score": 88.15
   },
   {
     "altitude": 1700,
     "variety": "Gesha",
     "score": 95.5,
     "price": 1029
   }
] )
填充范围内全部的值

下面的例子使用$densify为所有的咖啡variety填充altitude字段:

js 复制代码
db.coffee.aggregate( [
   {
      $densify: {
         field: "altitude",
         partitionByFields: [ "variety" ],
         range: {
            bounds: "full",
            step: 200
         }
      }
   }
] )

在上例的聚合中:

  • 根据variety对文档进行分区,创建了两个分组分别是'Arabica Typica'和Gesha咖啡。
  • 指定了full范围,即在每个分区的现有文档范围内对数据进行填充。
  • 指定了200step,即被创建的新文档的altitude间隔为200

聚合输出下面的文档:

js 复制代码
[
   {
     _id: ObjectId("618c031814fbe03334480475"),
     altitude: 600,
     variety: 'Arabica Typica',
     score: 68.3
   },
   {
     _id: ObjectId("618c031814fbe03334480476"),
     altitude: 750,
     variety: 'Arabica Typica',
     score: 69.5
   },
   { variety: 'Arabica Typica', altitude: 800 },
   {
     _id: ObjectId("618c031814fbe03334480477"),
     altitude: 950,
     variety: 'Arabica Typica',
     score: 70.5
   },
   { variety: 'Gesha', altitude: 600 },
   { variety: 'Gesha', altitude: 800 },
   { variety: 'Gesha', altitude: 1000 },
   { variety: 'Gesha', altitude: 1200 },
   {
     _id: ObjectId("618c031814fbe03334480478"),
     altitude: 1250,
     variety: 'Gesha',
     score: 88.15
   },
   { variety: 'Gesha', altitude: 1400 },
   { variety: 'Gesha', altitude: 1600 },
   {
     _id: ObjectId("618c031814fbe03334480479"),
     altitude: 1700,
     variety: 'Gesha',
     score: 95.5,
     price: 1029
   },
   { variety: 'Arabica Typica', altitude: 1000 },
   { variety: 'Arabica Typica', altitude: 1200 },
   { variety: 'Arabica Typica', altitude: 1400 },
   { variety: 'Arabica Typica', altitude: 1600 }
 ]
在所有分区填充值

下面使用$densify只对每个varietyaltitude字段的间隙进行填充:

js 复制代码
db.coffee.aggregate( [
   {
      $densify: {
         field: "altitude",
         partitionByFields: [ "variety" ],
         range: {
            bounds: "partition",
            step: 200
         }
      }
   }
] )

这个例子中的聚合:

  • 根据variety对文档进行分区,创建了两个分组分别是'Arabica Typica'和Gesha咖啡。
  • 指定分区范围,即在每个分区内对数据进行填充处理。
    • 对于Arabica Typica分区,范围是600~900
    • 对于Gesha分区,范围是1250~1700
  • 指定了200step,即被创建的新文档的altitude间隔为200

聚合输出下面的文档:

js 复制代码
[
   {
     _id: ObjectId("618c031814fbe03334480475"),
     altitude: 600,
     variety: 'Arabica Typica',
     score: 68.3
   },
   {
     _id: ObjectId("618c031814fbe03334480476"),
     altitude: 750,
     variety: 'Arabica Typica',
     score: 69.5
   },
   { variety: 'Arabica Typica', altitude: 800 },
   {
     _id: ObjectId("618c031814fbe03334480477"),
     altitude: 950,
     variety: 'Arabica Typica',
     score: 70.5
   },
   {
     _id: ObjectId("618c031814fbe03334480478"),
     altitude: 1250,
     variety: 'Gesha',
     score: 88.15
   },
   { variety: 'Gesha', altitude: 1450 },
   { variety: 'Gesha', altitude: 1650 },
   {
     _id: ObjectId("618c031814fbe03334480479"),
     altitude: 1700,
     variety: 'Gesha',
     score: 95.5,
     price: 1029
   }
 ]
相关推荐
RestCloud2 分钟前
StarRocks 数据分析加速:ETL 如何实现实时同步与高效查询
数据库
野猪亨利6671 小时前
Qt day1
开发语言·数据库·qt
本就一无所有 何惧重新开始1 小时前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
isaki1371 小时前
qt day1
开发语言·数据库·qt
流星白龙1 小时前
【Qt】4.项目文件解析
开发语言·数据库·qt
小钻风33661 小时前
HTTPS是如何确保安全的
网络·数据库
CryptoPP2 小时前
获取越南股票市场列表(包含VN30成分股)实战指南
大数据·服务器·数据库·区块链
阿巴~阿巴~3 小时前
Redis重大版本演进全解析:从2.6到7.0
服务器·数据库·redis·ubuntu·缓存·centos
qq_404643344 小时前
MySQL中RUNCATE、DELETE、DROP 的基本介绍
数据库·mysql
RunningShare4 小时前
高可用架构实战:SpringBoot+MongoDB构建AI原生应用
spring boot·mongodb·架构