归档字段必须用ISODate而非字符串或时间戳,否则TTL索引静默失效;expireAfterSeconds:0表示按archivedAt字段值精确过期,但清理有最多60秒延迟;分片集群中TTL索引须包含分片键。

归档字段必须用 ISODate,不能用字符串或时间戳
MongoDB 的 TTL 索引只识别 ISODate 类型字段,如果存成字符串(如 "2024-05-01")或数字时间戳(如 1714521600000),createIndex({ archivedAt: 1 }, { expireAfterSeconds: 0 }) 会静默失效——查不到错误,但文档永远不会被删。
实操建议:
- 写入时统一用
new Date()或ISODate("2024-05-01T00:00:00Z"),避免在应用层拼字符串 - 已有字符串字段需批量转换:
db.logs.updateMany( { archivedAt: { $type: "string" } }, [ { $set: { archivedAt: { $dateFromString: { dateString: "$archivedAt" } } } } ] ) - 验证是否生效:用
db.logs.getIndexes()确认索引里expireAfterSeconds不为null
按月归档 ≠ 每月固定时间删,而是靠 expireAfterSeconds + 时间偏移计算
TTL 索引不支持“每月 1 号凌晨删上月数据”这种调度逻辑,它只做“文档创建后 N 秒删”。所以要实现按月归档,得把归档时间点提前算好,写进 archivedAt 字段。
例如:想让 5 月产生的日志在 6 月 1 日 00:00 归档,则 5 月任意一天写入的文档,archivedAt 都设为 ISODate("2024-06-01T00:00:00Z"),再建 TTL 索引:
db.logs.createIndex({ archivedAt: 1 }, { expireAfterSeconds: 0 })
关键点:
- 归档时间必须是未来时间(否则写入即删),且最好带时区(推荐 UTC)
- 不要用
$currentDate直接设archivedAt,它无法动态算下月 1 号;得在应用层或聚合管道里计算,比如用$dateFromParts - 如果归档策略变更为“保留最近 3 个月”,只需批量更新
archivedAt字段,无需改索引
避免 expireAfterSeconds: 0 导致的秒级误删
设 expireAfterSeconds: 0 表示“过期时间就是 archivedAt 字段值”,但 MongoDB 的 TTL 清理线程默认每 60 秒跑一次,且不保证精确到秒。如果 archivedAt 设的是今天 00:00:00,而清理线程在 00:00:59 才执行,那这分钟内的数据就多留了近一分钟——对金融、审计类场景可能出问题。
缓解办法:
- 归档时间点往前提 5 分钟(如设为
2024-06-01T23:55:00Z而非00:00:00Z),留出缓冲 - 关键业务不用纯 TTL,改为定时脚本 +
deleteMany主动删,并加explain("executionStats")确保走对索引 - 监控
db.serverStatus().metrics.ttl.deletedDocuments,突增说明归档节奏异常
分片集群下 TTL 索引有严重限制
在分片集群中,TTL 索引只能建在分片键(shard key)上,或与分片键有前缀关系的字段上。如果你的集合按 { tenantId: 1, createdAt: 1 } 分片,那 archivedAt 单独建 TTL 索引会报错:Cannot create TTL index on a sharded collection unless the index contains the shard key。
可行解法:
- 把
archivedAt加进分片键,比如改成{ tenantId: 1, archivedAt: 1 }(注意:分片键不可更改,需重建集合) - 放弃 TTL,用定期
deleteMany配合hint强制走archivedAt索引,但得自己控制并发和慢查询 - 归档逻辑下沉到应用层:写入时直接路由到按月命名的集合(如
logs_202405),删整表比删文档快得多
真正麻烦的不是怎么删,而是归档触发时机和分片路由的耦合——一旦选错分片键,后面补救成本远高于初期设计时多想两分钟。











