冷热标记应使用独立字符串字段datastatus并建单字段索引,避免复用业务字段或复合索引导致查询失效;冷数据占比低时需配合partialfilterexpression优化。

冷热标记字段必须独立且可索引
别把冷热状态塞进业务字段里(比如复用 status),否则查询和索引都混乱。直接加一个专用字段,如 dataStatus,值为 "hot" 或 "cold",并确保它被单独建索引。
常见错误现象:用 createdAt + 应用层判断“是否过期”,导致每次查冷数据都要全表扫描时间范围;或把 dataStatus 和 updatedAt 组成复合索引,但实际只按状态查,索引失效。
-
dataStatus字段类型统一用字符串(避免混用数字 0/1),便于阅读和调试 - 建单字段索引:
db.events.createIndex({ dataStatus: 1 }),别漏掉partialFilterExpression(见下一条) - 如果冷数据占比极低({ sparse: true } 配合
{ dataStatus: "cold" }
归档时间戳必须与原始时间字段解耦
归档不是简单删旧数据,而是迁移。所以不能只依赖 createdAt 做归档依据——它属于业务语义,而归档动作需要独立的、可审计的时间锚点。
使用场景:某条事件在 2025-03-01 写入,2026-06-15 被归档。若归档逻辑仍查 { createdAt: { $lt: ISODate("2025-06-01") } },可能漏掉中间延迟归档的批次;更糟的是,归档库若也用 createdAt 建 TTL,会误删刚迁入的数据。
- 新增字段
archivedAt(Date 类型,初始为 null),归档时用$currentDate: { archivedAt: true }设置 - 冷库中建 TTL 索引必须基于
archivedAt:db.archived_events.createIndex({ archivedAt: 1 }, { expireAfterSeconds: 7 * 365 * 86400 }) - 热库中查待归档数据,应组合条件:
{ dataStatus: "hot", createdAt: { $lt: ISODate("...") }, archivedAt: null }
嵌套结构要按访问粒度拆分,否则冷热分离失效
如果一个文档里既含高频更新的实时状态(如 lastSeen),又含低频读取的完整历史轨迹(如 locationHistory 数组),那整个文档会被当作“热”处理——哪怕 95% 的内容半年都不动一次。
典型表现:冷热分离后,WiredTiger 缓存中 bytes currently in the cache 没下降,db.serverStatus().wiredtiger.cache 显示大量“冷内容”仍驻留内存。
- 把长生命周期子结构(如日志、轨迹、附件元信息)抽成独立集合,用
ObjectId引用 - 热集合只保留核心字段:
_id、deviceId、eventType、lastReportTime、dataStatus - 冷集合(或对象存储)存完整快照,字段名可带版本标识(如
v1_full_payload),避免后续结构变更影响归档兼容性
change stream 必须监听写操作,而非依赖 TTL 删除事件
TTL 索引触发的删除不发 fullDocument,change stream 只能拿到 documentKey,根本没法知道删的是什么内容——这对归档审计是致命缺陷。
容易踩的坑:以为给 createdAt 加了 TTL 就等于自动归档,结果 delete 事件没 payload,归档服务只能盲删,出错无法回溯。
- 归档流程必须主动发起:
find()批量拉取 → 写入冷库 → 记录归档日志(含docIds和archivedAt)→deleteMany() - change stream 监听目标是热库的
insert和update,用于捕获归档期间的变更,同步到冷库(数据一致性模块) - 归档日志集合(如
archive_log)本身也要建 TTL,但基于logTime字段,周期比业务数据长(如 90 天)
冷热分离真正卡住的地方,往往不是迁移逻辑,而是字段语义混乱和索引设计失当——dataStatus 字段有没有被所有写入口统一维护?archivedAt 是不是每次归档都可靠设置?这些细节一旦松动,整个分层就变成“看起来分了,实际没分”。











