小文档多导致磁盘IO高,因WiredTiger为每条文档单独分配页空间、生成独立key并维护细碎索引条目,引发严重存储碎片化,一次查询可能触发数十次随机磁盘读;文档桶模式通过按时间/业务聚合小文档为大文档(如每小时一个桶,data存数组),配合bucket_start/bucket_end索引与聚合管道下推查询($filter/$arrayElemAt),显著降低IO。

为什么小文档多会导致磁盘IO高
当集合里存着几十万条 {_id: ..., timestamp: ..., value: 1.23} 这类只有几字段的小文档时,WiredTiger 引擎会为每条文档单独分配页内空间、生成独立的内部 key,并在索引中维护大量细碎条目。结果就是:物理存储碎片化严重,一次逻辑查询可能触发数十次随机磁盘读(哪怕数据在内存页缓存里,OS 页缓存也难以有效合并这些分散访问)。
文档桶模式(Document Bucketing)怎么建
核心思路是把时间/业务上天然聚合的小文档,批量“压”进一个大文档里,用数组存原始数据,再加元信息辅助定位。比如传感器每秒上报一条温度,不存 86400 条文档,而是按小时聚合:
db.sensor_data.insertOne({
_id: "2026-05-11T08:00:00Z",
bucket_start: ISODate("2026-05-11T08:00:00Z"),
bucket_end: ISODate("2026-05-11T09:00:00Z"),
count: 3600,
data: [
{ t: ISODate("2026-05-11T08:00:01Z"), v: 23.4 },
{ t: ISODate("2026-05-11T08:00:02Z"), v: 23.5 },
// ... 共3600个元素
]
})
- 桶键
_id必须可排序、无歧义(推荐 ISO8601 字符串或时间戳),方便范围查询 -
data数组长度建议控制在 1000–5000,太大会拖慢单次读取和更新;太小则起不到减少 IO 的效果 - 务必在
bucket_start和bucket_end上建索引,否则查某段时间仍要 scan 所有桶
查询时怎么避免反模式操作
桶模式下,find() 返回的是整个桶文档,但你通常只想要其中部分 data 元素。别这么干:
// ❌ 错误:全量拉取后在应用层过滤
const bucket = await db.sensor_data.findOne({ bucket_start: { $lte: targetTime }, bucket_end: { $gt: targetTime } });
const point = bucket.data.find(d => d.t.getTime() === targetTime.getTime());
应该用聚合管道直接在数据库内切片:
// ✅ 正确:用 $filter + $arrayElemAt 下推计算
db.sensor_data.aggregate([
{ $match: { bucket_start: { $lte: targetTime }, bucket_end: { $gt: targetTime } } },
{ $project: {
point: { $arrayElemAt: [
{ $filter: {
input: "$data",
cond: { $eq: ["$$this.t", targetTime] }
}
},
0
] }
}
}
])
- 用
$filter而不是$unwind:后者会爆炸式展开数组,内存和 CPU 开销剧增 - 如果需要按时间范围查多个点(如最近 5 分钟所有值),用
$slice配合已知桶内偏移,比$filter更快 - 注意:WiredTiger 对大文档的压缩率很高,但超过 16MB 会报错,桶文档必须留足余量
写入和更新的现实约束
桶模式让读快了,但写逻辑变复杂——你得先查桶是否存在,再决定是 insertOne 还是 updateOne 追加。容易踩的坑:
- 并发写同一桶时,
$push可能导致数组重复或顺序错乱,必须加upsert: true+ 唯一条件判断,或改用$addToSet(但需确保t字段唯一) - 不要对
data数组做$pop或$pull:删除操作会重写整个文档,IO 放大 - 如果业务要求高频更新单个点(如修正某时刻错误值),桶模式反而更慢——这时应退回到单文档 + 合适索引
桶不是银弹。它真正起效的前提是:读远多于写、数据天然有时序局部性、且应用能接受“桶内查询延迟”(比如查某分钟数据,返回的是整分钟桶,而不是精确到秒的单点)。一旦这三个条件有任一不满足,就该重新评估设计。


















