bigywater.tech
MongoDB · 22

时序集合:bucket 文档长什么样

时序集合里的数据到底怎么存?这篇用 3 个传感器的测量,通过 rawData 读出底层 bucket 文档的 control、meta、data,再验证 bucket 的拆分上限、磁盘占用和 update / delete 的限制。

1200 → 3
3 个传感器各 400 个测量,底层只有 3 个 bucket 文档
1000
单个 bucket 最多的测量数,3000 个测量拆成 3 个 bucket
2%
周期性合成数据下时序集合占盘是普通集合的比例(真实数据会高得多)

创建与不可直接访问

实测 · 本机执行输出
  test> db.createCollection("m", {timeseries: {timeField: "ts", metaField: "sensor", granularity: "seconds"}})
  { ok: 1 }
  test> db.getCollectionInfos().map(c => ({name: c.name, type: c.type, ts: c.options.timeseries}))
  [
    {
      name: 'm',
      type: 'timeseries',
      ts: {
        timeField: 'ts',
        metaField: 'sensor',
        granularity: 'seconds',
        bucketMaxSpanSeconds: 3600,
        fixedBucketing: true
      }
    }
  ]
  test> db.getSiblingDB("test").runCommand({listCollections: 1, filter: {name: /system.buckets/}}).cursor.firstBatch.map(c => c.name)
  []
  test> try { db.system.buckets.m.countDocuments() } catch (e) { e.message.slice(0, 260) }
  Command aggregate on namespace test.system.buckets.m is not supported. Direct access to timeseries buckets namespaces is not allowed anymore. Please target the main times

时序集合在 listCollections 里是 timeseries 类型,选项带 bucketMaxSpanSeconds。9.0.2 里不能再直接访问 system.buckets.*,要读底层数据用命令的 rawData: true 选项。

测量与 bucket

实测 · 本机执行输出
  插入了 1200 个测量文档;db.m.countDocuments = 1200
  test> db.runCommand({aggregate: "m", cursor: {}, rawData: true, pipeline: [{$count: "buckets"}]}).cursor.firstBatch
  [ { buckets: 3 } ]
  test> db.runCommand({aggregate: "m", cursor: {}, rawData: true, pipeline: [{$project: {_id: 0, sensor: "$meta", n: "$control.count", version: "$control.version", first: "$control.min.ts", last: "$control.max.ts"}}, {$sort: {sensor: 1}}]}).cursor.firstBatch
  [
    {
      sensor: 'A',
      n: 400,
      version: 2,
      first: ISODate('2026-01-01T00:00:00.000Z'),
      last: ISODate('2026-01-01T00:06:39.000Z')
    },
    {
      sensor: 'B',
      n: 400,
      version: 2,
      first: ISODate('2026-01-01T00:00:00.000Z'),
      last: ISODate('2026-01-01T00:06:39.000Z')
    },
    {
      sensor: 'C',
      n: 400,
      version: 2,
      first: ISODate('2026-01-01T00:00:00.000Z'),
      last: ISODate('2026-01-01T00:06:39.000Z')
    }
  ]

1200 个测量只存成 3 个 bucket 文档:每个传感器(metaField 相同)的测量被放进同一个 bucket。

bucket 的结构

实测 · 本机执行输出
  test> db.runCommand({find: "m", filter: {meta: "A"}, limit: 1, rawData: true}).cursor.firstBatch[0]
  {
    _id: ObjectId('6955b90052ab9a4f9559d885'),
    control: {
      version: 2,
      min: {
        _id: ObjectId('6acc3ca0a407339f0e2e5134'),
        ts: ISODate('2026-01-01T00:00:00.000Z'),
        temp: 20,
        hum: 30
      },
      max: {
        _id: ObjectId('6acc3ca0a407339f0e2e55dc'),
        ts: ISODate('2026-01-01T00:06:39.000Z'),
        temp: 25,
        hum: 69
      },
      count: 400
    },
    meta: 'A',
    data: {
      ts: Binary.createFromBase64('CQAAqNp2mwEAAIMKfQAAAAAAAC8AAAAAAAAAAgAAAAAAAAALAAAAAAAAAAA=', 7),
      _id: Binary.createFromBase64('BwBqzDygpAcznw4uUZiPvQMAALgBIAD9CQABEP8/ANzcf0ECRBsArOZ/MgDQ9p+M+7
      temp: Binary.createFromBase64('AQBI4XoUroc0QL8p9DIBvMZFIOmFANEQWN8J+QUsyLB3QUm5mI3pzDODRpkXJ/lYg
      hum: Binary.createFromBase64('EAA/AAAAj/YJVDFqh0iMlomHg6wCGMpW2ukVEKazhAcmgGQVgBVtplSEtFQh/h4HAe
    }
  }
  test> db.runCommand({aggregate: "m", cursor: {}, rawData: true, pipeline: [{$match: {meta: "A"}}, {$limit: 1}, {$project: {_id: 0, version: "$control.version", ts: {$type: "$data.ts"}, temp: {$type: "$data.temp"}, hum: {$type: "$data.hum"}, sensor: {$type: "$meta"}, ts_bytes: {$binarySize: "$data.ts"}, temp_bytes: {$binarySize: "$data.temp"}}}]}).cursor.firstBatch
  [
    {
      version: 2,
      ts: 'binData',
      temp: 'binData',
      hum: 'binData',
      sensor: 'string',
      ts_bytes: 44,
      temp_bytes: 552
    }
  ]

bucket 文档的 control 记录版本、每个字段的最小/最大值和计数;meta 存元数据;data 每个字段一列,以压缩的二进制存放。

bucket 上限

实测 · 本机执行输出
  test> db.runCommand({aggregate: "m2", cursor: {}, rawData: true, pipeline: [{$project: {_id: 0, n: "$control.count", from: "$control.min.ts", to: "$control.max.ts"}}, {$sort: {from: 1}}]}).cursor.firstBatch
  [
    {
      n: 1000,
      from: ISODate('2026-01-01T00:00:00.000Z'),
      to: ISODate('2026-01-01T00:16:39.000Z')
    },
    {
      n: 1000,
      from: ISODate('2026-01-01T00:16:00.000Z'),
      to: ISODate('2026-01-01T00:33:19.000Z')
    },
    {
      n: 1000,
      from: ISODate('2026-01-01T00:33:00.000Z'),
      to: ISODate('2026-01-01T00:49:59.000Z')
    }
  ]
  test> var t = db.m2.aggregate([{$collStats: {storageStats: {}}}]).toArray()[0].storageStats.timeseries; ({bucketCount: t.bucketCount, numBucketInserts: t.numBucketInserts, numBucketsClosedDueToCount: t.numBucketsClosedDueToCount, numBucketsClosedDueToSize: t.numBucketsClosedDueToSize, avgNumMeasurementsPerCommit: t.avgNumMeasurementsPerCommit})
  {
    bucketCount: 3,
    numBucketInserts: 3,
    numBucketsClosedDueToCount: 2,
    numBucketsClosedDueToSize: 0,
    avgNumMeasurementsPerCommit: 1000
  }

单个 bucket 最多 1000 个测量,3000 个每秒一个的测量拆成 3 个 bucket;统计里能看到因数量关闭的 bucket 数。

占盘

实测 · 本机执行输出
  时序集合: storageSize 24,576,bucket 数 60,平均每个 bucket 714
  普通集合: storageSize 1,318,912,文档数 60,000
  比例: 时序集合是普通集合的 2%

列存加压缩让时序集合远小于普通集合;本文数据是周期性合成的,压缩得格外好,不能当作真实数据的比例。

查询与限制

实测 · 本机执行输出
  test> var p = db.m.explain().aggregate([{$match: {sensor: "A", ts: {$gte: ISODate("2026-01-01T00:00:10Z"), $lt: ISODate("2026-01-01T00:00:20Z")}}}]); JSON.stringify(p.stages ? p.stages.map(s => Object.keys(s)[0]) : p.queryPlanner.winningPlan.stage)
  ["$cursor","$_internalUnpackBucket"]
  test> db.m.getIndexes()
  [
    {
      v: 2,
      key: { sensor: 1, ts: 1 },
      name: 'sensor_1_ts_1',
      collation: { locale: 'simple' }
    }
  ]
  test> db.m.updateOne({sensor: "A"}, {$set: {temp: 0}})
  Cannot perform a non-multi update on a time-series collection
  test> db.m.updateMany({sensor: "A"}, {$set: {sensor: "A2"}})
  {
    acknowledged: true,
    insertedId: null,
    matchedCount: 400,
    modifiedCount: 400,
    upsertedCount: 0
  }
  test> db.m.deleteOne({temp: 20})
  { acknowledged: true, deletedCount: 1 }
  test> db.m.deleteMany({sensor: "B"})
  { acknowledged: true, deletedCount: 400 }

查询先扫 bucket 再用 $_internalUnpackBucket 展开;自动建 meta + 时间的索引;非 multi 的 update 不允许,按 meta 的 updateMany / deleteMany 可以。

分步演示

时序集合的 bucket
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。bucket 文档通过 rawData: true 读取,测量时间是固定的 2026-01-01 起每秒一个;磁盘占用在 fsync 之后读 collStats
    • 没有验证:granularity 为 minutes 或 hours 时的 bucket 时间跨度;metaField 是对象或数组时的行为;乱序(过去的时间)插入导致的 bucket 重开;storageSize 差距在真实传感器数据上的比例;为什么第二个 bucket 的 control.min.ts 会向下取整到整分钟(只观察到现象);update 与 delete 的完整限制(这里只试了四条语句);时序集合上的二级索引与分片
    京ICP备2026067502号-1京公网安备11010502063487号