时序集合里的数据到底怎么存?这篇用 3 个传感器的测量,通过 rawData 读出底层 bucket 文档的 control、meta、data,再验证 bucket 的拆分上限、磁盘占用和 update / delete 的限制。
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 选项。
插入了 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。
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 每个字段一列,以压缩的二进制存放。
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 可以。