MongoDB时序集合

MongoDB时序集合

时序数据

时序数据就是一系列随着时间变化的数据。时序数据由3个部分组件

  • 时间。数据记录的时间
  • 元数据。有时也叫做数据的来源。由一些列标签或者标记(label or tag)标识唯一的时序数据。很少发生改变。
  • 测量值。有时也叫做度量或者值。随着时间变化的数据点,通常以键值对来表示。

时序集合

时序集合可以高效地存储同一来源的数据,并按相近时间存储。

优点

同普通集合相比,时序集合查询效率更好,占用磁盘空间更低。从Mongodb6.3开始,会自动创建一个时间和元数据的组合索引。

时序集合使用列存储并按时间序存储时序数据。使用列存储有以下好处。

  • 减少处理时序数据的复杂度。
  • 提高查询效率
  • 减少磁盘的存储空间
  • 减少读操作的IO请求
  • 提高WiredTiger缓存的使用率
行为

时序集合和普通集合非常相似,可以像普通集合一样进行插入和查询。

mongodb使用内部集合,把时序集合当作可写但不会持久化的视图。当插入数据,内部集合会自动把时序数据转成优化后的存储格式。

查询时序数据时,时序集合会充分利用内部优化的存储格式来快速返回。

时序集合的简单操作

创建时序集合

  1. 创建时序集合
sh 复制代码
db.createCollection(
"weather",
{
  timeseries: {
  timeField: "timestamp",
  metaField: "metadata"
}})
  1. 设置时间字段和元数据字段
sh 复制代码
timeseries: {
   timeField: "timestamp",
   metaField: "metadata"
}
  1. 定义数据的时间间隔,分别用以下2种方式。
    定义granularity字段
sh 复制代码
timeseries: {
   timeField: "timestamp",
   metaField: "metadata",
   granularity: "seconds"
}

或者 在Mongodb6.3以上版本定义bucketMaxSpanSeconds和bucketRoundingSeconds字段。这2个字段的值必须一样。

sh 复制代码
timeseries: {
   timeField: "timestamp",
   metaField: "metadata",
   bucketMaxSpanSeconds: "300",
   bucketRoundingSeconds: "300"
}
  1. 可选择设置expireAfterSeconds字段表示当timeField字段的值太久了导致文档过期。
sh 复制代码
timeseries: {
   timeField: "timestamp",
   metaField: "metadata",
   granularity: "seconds",
   expireAfterSeconds: "86400"
}

插入测量值到时序集合

这里的例子,每个文档只有一个数据点。使用的是批量插入。

sh 复制代码
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
   }
] )

查询时序集合

查询时序集合和普通集合的方式一样。

这里的例子只返回一个文档

sh 复制代码
db.weather.findOne({
   "timestamp": ISODate("2021-05-18T00:00:00.000Z")
})

输出结果

sh 复制代码
{
   timestamp: ISODate("2021-05-18T00:00:00.000Z"),
   metadata: { sensorId: 5578, type: 'temperature' },
   temp: 12,
   _id: ObjectId("62f11bbf1e52f124b84479ad")
}

向时序集合执行聚合操作

sh 复制代码
db.weather.aggregate( [
   {
      $project: {
         date: {
            $dateToParts: { date: "$timestamp" }
         },
         temp: 1
      }
   },
   {
      $group: {
         _id: {
            date: {
               year: "$date.year",
               month: "$date.month",
               day: "$date.day"
            }
         },
         avgTmp: { $avg: "$temp" }
      }
   }
] )

这个聚合操作使用时间来聚合数据并返回温度的平均值

sh 复制代码
{
  "_id" : {
    "date" : {
      "year" : 2021,
      "month" : 5,
      "day" : 18
    }
  },
  "avgTmp" : 12.714285714285714
}
{
  "_id" : {
    "date" : {
      "year" : 2021,
      "month" : 5,
      "day" : 19
    }
  },
  "avgTmp" : 13
}

时序集合的自动删除

手动激活时序集合自定过期删除

sh 复制代码
db.runCommand({
   collMod: "weather24h",
   expireAfterSeconds: 604801
})

改变时序集合的过期时间

sh 复制代码
db.runCommand({
   collMod: "weather24h",
   expireAfterSeconds: 604801
})

取消自定过期删除

sh 复制代码
db.runCommand({
    collMod: "weather24h",
    expireAfterSeconds: "off"
})

过期自动删除行为

MongoDB不保证过期数据会立马删除。一旦一个桶的所有文档的都过期,删除过期桶的后台任务要到下一次运行才会删除。一个桶存储的时序数据的时间跨度,是根据时序集合的granularity字段来决定的。

granularity(细粒度) 时间跨度
seconds(秒) 1小时
minutes(分钟) 24小时
hours (小时) 30天

**后台删除任务每60s执行一次。因此,过期的文档在这周期内还会存在在集合中。这个周期涉及文档的过期时间,桶里面其他文档的过期时间以及后台任务的运行情况。

**

因为删除数据的周期涉及到mongodb实例的工作负载,过期数据的存在可能会超过后台运行周期的60s。

设置时序集合的时间细粒度

当你创建时序集合,mongodb会自动创建一个system.buckets的系统集合。把所有的时序数据合并到bukects(桶)里面。通过设置时间细粒度,控制数据装到桶里的频率。这个频率一般根据数据的采集频率。

从Mongodb6.3开始,可以设置bucketMaxSpanSeconds和bucketRoundingSeconds参数来自定义桶的边界。更精确地控制时序数据多长时间装桶。

使用granularity字段

granularity的值,决定桶的最大时间间隔

granularity(细粒度) 桶的时间跨度
seconds(秒) 1小时
minutes(分钟) 24小时
hours (小时) 30天

granularity的默认值为秒。修改granularity的值为接近实际数据采集的频率值,可以提高时序集合的性能。如果使用1000个传感器记录天气数据,但每个传感器每5分钟采集一次。应该设置granularity值为"minutes"

sh 复制代码
db.createCollection(
    "weather24h",
    {
       timeseries: {
          timeField: "timestamp",
          metaField: "metadata",
          granularity: "minutes"
       },
       expireAfterSeconds: 86400
    }
)

上面的例子,如果granularity值设置为hours,那么一个月的天气数据都会进入到一个桶里面。这样会造成更长的遍历时间和更慢的查询。如果granularity值设置为seconds,会导致一个周期内(5分钟)有更多的桶。大部分的桶可能只包含一个文档。

使用自定义的装桶参数

在Mongodb6.3以上版本。装桶参数,除了granularity参数外,还可以设置2个参数来手动设置桶的边界。通常在需要更加精确地优化大量查询和插入数据的性能时使用。

使用自定义装桶参数,设置这2个参数为相同值,而且不要设置granularity。

  • bucketMaxSpanSeconds。设置同一个桶,数据的时间差的最大值。即最大时间跨度。值为1-31536000。
  • bucketRoundingSeconds 这个值确定桶的开始时间。当一个文档路由到一个新桶,Mongdb会使用bucketRoundingSeconds来取整这个文档的时间戳并设置这个桶的最小时间(开始时间)。

对于5分钟采集一次的天气数据的例子,可以设置自定义装桶参数为300秒(5分钟),而不是设置granularity值为minutes。

sh 复制代码
db. createCollection(
   "weather24h",
   {
      timeseries: {
         timeField: "timestamp",
         metaField: "metadata",
         bucketMaxSpanSeconds: 300,
         bucketRoundingSeconds: 300
      }
   }
)

如果一个文档的时间戳为2023-03-27T18:24:35Z且没有一个现成的桶符合条件。Mongdb会创建一个新桶,并设置桶的最小时间为2023-03-27T18:20:00Z,最大时间为2023-03-27T18:24:59Z。

改变时序集合细粒度

提高时序集合的granularity。

sh 复制代码
db.runCommand({
   collMod: "weather24h",
   timeseries: { granularity: "seconds" || "minutes" || "hours" }
})

或者 提高bucketMaxSpanSeconds 和 bucketMaxSpanSeconds值

sh 复制代码
db.runCommand({
   collMod: "weather24h",
   timeseries: {
      bucketRoundingSeconds: "86400",
      bucketMaxSpanSeconds: "86400"
   }
})

不能降低granularity、bucketMaxSpanSeconds、bucketMaxSpanSeconds值

为时序集合添加索引

注意:Mongodb默认创建_id唯一索引。文档说的辅助的第二索引。这里统一叫做索引。

为了提高查询效率,可以为时序集合创建更多的索引来支持通用的查询。从MongoDB6.3开始,MongoDb会自动创建一个时间和元数据的复合索引。

以下天气数据的例子,你可能会考虑创建一个新的索引。

sh 复制代码
db.createCollection(
"weather",
{
   timeseries: {
      timeField: "timestamp",
      metaField: "metadata"
}})

metadata字段是一个子文档。包含传感器ID和类型。

sh 复制代码
{
   "timestamp": ISODate("2021-05-18T00:00:00.000Z"),
   "metadata": {
     "sensorId": 5578,
     "type": "temperature"
   },
   "temp": 12
}

默认的复合索引只会索引整个metadata的子文档。所以这个索引只能使用$eq操作符来查询。通过对metadata的子文档字段建立索引,来提高metadata查询性能。

例如, 这个$in查询会被metadata.type的索引提高性能。

sh 复制代码
{ metadata.type:{ $in: ["temperature", "pressure"] }}

使用索引来提高排序性能

对时序集合的排序,会使用timeField的索引。在某些条件下,排序操作可能会使用metaField和timeField的复合索引。

聚合操作的match and sort决定了时序集合使用哪个索引。下面的例子可能会使用以下索引。

  • 对{ : ±1 } 排序,使用索引
  • 对{ : ±1, timeField: ±1 }排序,使用默认的{ : ±1, timeField: ±1 }复合索引。
  • 对{ : ±1 }排序,并且使用匹配。使用{ metaField: ±1, timeField: ±1 }复合索引。

迁移数据到时序集合

  1. 如果原来的集合没有metadata元数据字段。使用 $addFields 聚合操作添加。
    这个是原来的集合
sh 复制代码
{
    "_id" : ObjectId("5553a998e4b02cf7151190b8"),
    "st" : "x+47600-047900",
    "ts" : ISODate("1984-03-05T13:00:00Z"),
    "position" : {
      "type" : "Point",
      "coordinates" : [ -47.9, 47.6 ]
    },
    "elevation" : 9999,
    "callLetters" : "VCSZ",
    "qualityControlProcess" : "V020",
    "dataSource" : "4",
    "type" : "FM-13",
    "airTemperature" : { "value" : -3.1, "quality" : "1" },
    "dewPoint" : { "value" : 999.9, "quality" : "9" },
    "pressure" : { "value" : 1015.3, "quality" : "1" },
    "wind" : {
      "direction" : { "angle" : 999, "quality" : "9" },
      "type" : "9",
      "speed" : { "rate" : 999.9, "quality" : "9" }
    },
    "visibility" : {
      "distance" : { "value" : 999999, "quality" : "9" },
      "variability" : { "value" : "N", "quality" : "9" }
    },
    "skyCondition" : {
      "ceilingHeight" : { "value" : 99999, "quality" : "9", "determination" : "9" },
      "cavok" : "N"
    },
    "sections" : [ "AG1" ],
    "precipitationEstimatedObservation" : { "discrepancy" : "2",
    "estimatedWaterDepth" : 999 }
}
  1. 使用 $project来包含或者移除字段。
sh 复制代码
{ $addFields: {
    metaData: {
      "st": "$st",
      "position": "$position",
      "elevation": "$elevation",
      "callLetters": "$callLetters",
      "qualityControlProcess": "$qualityControlProcess",
      "type": "$type"
    }
  },
},
{ $project: {
    _id: 1,
    ts: 1,
    metaData: 1,
    dataSource: 1,
    airTemperature: 1,
    dewPoint: 1,
    pressure: 1,
    wind: 1,
    visibility: 1,
    skyCondition: 1,
    sections: 1,
    precipitationEstimatedObservation: 1
  }
}
  1. 使用聚合操作符$out把集合的数据迁移到时序集合
sh 复制代码
db.weather_data.aggregate([
  {
     $addFields: {
       metaData: {
         "st": "$st",
         "position": "$position",
         "elevation": "$elevation",
         "callLetters": "$callLetters",
         "qualityControlProcess": "$qualityControlProcess",
         "type": "$type"
       }
     },
  }, {
     $project: {
        _id: 1,
        ts: 1,
        metaData: 1,
        dataSource: 1,
        airTemperature: 1,
        dewPoint: 1,
        pressure: 1,
        wind: 1,
        visibility: 1,
        skyCondition: 1,
        sections: 1,
        precipitationEstimatedObservation: 1
     }
  }, {
     $out: {
       db: "mydatabase",
       coll: "weathernew",
       timeseries: {
         timeField: "ts",
         metaField: "metaData"
       }
     }
  }
])

对时序集合分片

不能重分片以及分片的时序集合。但是可以重新定义分片键。

建立分片时序集合

  1. 连接分片集群
    使用mongsh连接 mongos的分片集合
sh 复制代码
mongosh --host <hostname> --port <port>
  1. 确定数据库启动分片
sh 复制代码
sh.status()
sh 复制代码
--- Sharding Status ---
   sharding version: {
      "_id" : 1,
      "minCompatibleVersion" : 5,
      "currentVersion" : 6,
...
  1. 创建时序集合
sh 复制代码
sh.shardCollection(
   "test.weather",
   { "metadata.sensorId": 1 },
   {
      timeseries: {
         timeField: "timestamp",
         metaField: "metadata",
         granularity: "hours"
      }
   }
)

分片键为 metadata.sensorId

对已经存在的时序集合进行分片

  1. 使用mongsh连接 mongos的分片集合
sh 复制代码
mongosh --host <hostname> --port <port>
  1. 确定数据库启动分片
sh 复制代码
sh.status()
sh 复制代码
--- Sharding Status ---
   sharding version: {
      "_id" : 1,
      "minCompatibleVersion" : 5,
      "currentVersion" : 6,
  1. 使用shardCollection()分片时序集合
sh 复制代码
sh.shardCollection( "test.deliverySensor", { "metadata.location": 1 } )

分片键为 metadata.sensorId

时序数据库的最佳实践

优化插入

按元数据批量处理文档
  • 避免网络往返,使用单个insertMany()比多个insertOne()好。
  • 尽可能按元数据的排序多个测量值(文档)。
    例如,有2个传感器A、B,一个传感器的多个测量值只会花费一个插入,而不是每个测量值多个插入。(这里的测量值是文档)

下面的批量插入6个文档的操作,实际只会产生2次插入。因为这些文档都按传感器排序

sh 复制代码
db.temperatures.insertMany( [
   {
      "metadata": {
         "sensor": "sensorA"
      },
      "timestamp": ISODate("2021-05-18T00:00:00.000Z"),
      "temperature": 10
   },
   {
      "metadata": {
         "sensor": "sensorA"
      },
      "timestamp": ISODate("2021-05-19T00:00:00.000Z"),
      "temperature": 12
   },
   {
      "metadata": {
         "sensor": "sensorA"
      },
      "timestamp": ISODate("2021-05-20T00:00:00.000Z"),
      "temperature": 13
   },
   {
      "metadata": {
         "sensor": "sensorB"
      },
      "timestamp": ISODate("2021-05-18T00:00:00.000Z"),
      "temperature": 20
   },
   {
      "metadata": {
         "sensor": "sensorB"
      },
      "timestamp": ISODate("2021-05-19T00:00:00.000Z"),
      "temperature": 25
   },
   {
      "metadata": {
         "sensor": "sensorB"
      },
      "timestamp": ISODate("2021-05-20T00:00:00.000Z"),
      "temperature": 26
   }
] )
使用一致的文档顺序

批量插入的文档,字段都保持一致的顺序会提高插入效率。

sh 复制代码
{
   "_id": ObjectId("6250a0ef02a1877734a9df57"),
   "timestamp": ISODate("2020-01-23T00:00:00.441Z"),
   "name": "sensor1",
   "range": 1
},
{
   "_id": ObjectId("6560a0ef02a1877734a9df66"),
   "timestamp": ISODate("2020-01-23T01:00:00.441Z"),
   "name": "sensor1",
   "range": 5
}

相反,字段顺序不一致,就没有得到优化。

sh 复制代码
{
   "range": 1,
   "_id": ObjectId("6250a0ef02a1877734a9df57"),
   "name": "sensor1",
   "timestamp": ISODate("2020-01-23T00:00:00.441Z")
},
{
   "_id": ObjectId("6560a0ef02a1877734a9df66"),
   "name": "sensor1",
   "timestamp": ISODate("2020-01-23T01:00:00.441Z"),
   "range": 5
}
增加客户端的个数

提高写入时序集合的客户端个数,会提高插入性能。

注意:必须禁用重试写入,否则时序集合不会合并多个客户端的写入。

优化压缩率

省略文档中的空对象和空数组。
sh 复制代码
{
 "timestamp": ISODate("2020-01-23T00:00:00.441Z"),
 "coordinates": [1.0, 2.0]
},
{
   "timestamp": ISODate("2020-01-23T00:00:10.441Z"),
   "coordinates": []
},
{
   "timestamp": ISODate("2020-01-23T00:00:20.441Z"),
   "coordinates": [3.0, 5.0]
}

上面这个例子,coordinates字段存在有值数组和空数组,会造成压缩器的schema发生改变。schema改变造成第2和第3个文档没有压缩。

相反,下面的例子省略了空数组,会有利于压缩器的压缩

sh 复制代码
{
   "timestamp": ISODate("2020-01-23T00:00:00.441Z"),
   "coordinates": [1.0, 2.0]
},
{
   "timestamp": ISODate("2020-01-23T00:00:10.441Z")
},
{
   "timestamp": ISODate("2020-01-23T00:00:20.441Z"),
   "coordinates": [3.0, 5.0]
}
减少数据的小数点位数

根据应用的情况确定保留的小数点位。更少的小数点位可以提高压缩率。

优化查询

设置适当的桶细粒度

创建时序集合,Mongodb会合并时序数据到桶里。精确地设置细粒度,能够控制数据的装桶频率,通常基于数据的采集频率。

从Mongdb6.3开始,可以设置自定义装桶参数bucketMaxSpanSeconds和bucketRoundingSeconds来指定桶的边界,更精确地控制时序数据的装桶。

创建索引

为了提高查询效率,为timeField和metaField建立索引可以支持更通用的查询。从MongoDb6.3起,默认创建timeField和metaField的复合索引。

相关推荐
码农捻旧3 分钟前
Node.js中MongoDB连接的进阶模块化封装
数据库·mongodb·node.js
洛阳泰山1 天前
Windows系统部署MongoDB数据库图文教程
数据库·windows·mongodb
yuanpan1 天前
MongoDB与PostgreSQL两个数据库的特点详细对比
数据库·mongodb·postgresql
白露与泡影1 天前
基于Mongodb的分布式文件存储实现
分布式·mongodb·wpf
孤的心了不冷1 天前
【Linux】Linux安装并配置MongoDB
linux·运维·mongodb·容器
好吃的肘子2 天前
MongoDB 应用实战
大数据·开发语言·数据库·算法·mongodb·全文检索
独泪了无痕2 天前
MongoTemplate 基础使用帮助手册
spring boot·mongodb
好吃的肘子2 天前
MongoDB入门
数据库·mongodb
柳如烟@3 天前
在Rocky Linux 9.5上部署MongoDB 8.0.9:从安装到认证的完整指南
linux·运维·mongodb
好吃的肘子3 天前
MongoDB 高可用复制集架构
数据库·mongodb·架构