TTL索引仅在字段为Date类型、单字段升序索引、字段值非null且存在三者同时满足时才生效;删除有最多60秒延迟,仅主节点执行,不触发变更流。

直接说结论:TTL索引不是“设了就自动删”,它只对 Date 类型字段 + 单字段升序索引 + 字段值非空这三者同时满足时才生效;实际删除有最多 60 秒延迟,且仅在主节点运行。
为什么文档写了过期时间却一直不删?
最常见原因是字段类型不对。MongoDB 的 TTL 索引完全不识别字符串或数字时间戳,哪怕你存的是 "2026-07-06T12:00:00Z" 或 1751832000000,也不会触发清理。
- 验证方式:
db.collection.findOne().expireAt.constructor.name必须返回Date - 错误写法:
expireAt: "2026-07-06T12:00:00Z"(字符串)或expireAt: 1751832000000(毫秒数) - 正确写法:
expireAt: new Date("2026-07-06T12:00:00Z")或new Date(Date.now() + 30 * 60 * 1000) - 批量修复字符串字段:
db.collection.updateMany({ expireAt: { $type: "string" } }, [ { $set: { expireAt: { $toDate: "$expireAt" } } } ])
如何创建真正起作用的TTL索引?
必须用 createIndex() 显式创建,且方向只能是升序 1;降序 -1 或复合索引会静默失败——不报错,但永不清理。
- 合法写法:
db.sessions.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 }) - 非法写法:
db.sessions.createIndex({ "expiresAt": -1 }, { expireAfterSeconds: 0 }) - 非法写法:
db.sessions.createIndex({ "status": 1, "expiresAt": 1 }, { expireAfterSeconds: 0 }) -
expireAfterSeconds: 0表示“过期时间就是字段值本身”,语义清晰,推荐统一用这种模式 - 分片集群中,TTL 索引需在每个分片上单独创建,MongoDB 不会自动同步
插入时怎么算“30分钟后过期”?
TTL 不看插入时间,只认你存进字段的那个 Date 值。想实现“写入后 N 秒过期”,就得把未来时间点算好再写进去。
- 错误做法:字段存
new Date(),然后设expireAfterSeconds: 1800→ 实际是“当前时间 + 1800 秒后过期”,但字段值已固定,后续所有文档都按同一基准时间删 - 正确做法:插入时动态计算:
{ expiresAt: new Date(Date.now() + 30 * 60 * 1000) } - Node.js 示例:
expiresAt: new Date(Date.now() + 30 * 60 * 1000),确保每次插入都是不同时间点 - 注意:字段名必须一致,且所有文档都要有该字段、不能为
null或缺失
真正容易被忽略的是:TTL 清理器只在主节点运行,不触发变更流,也不发任何通知;如果你的应用依赖“删完立刻感知”,得自己加轮询或状态标记,不能只靠索引兜底。









