TTL索引长期不删除文档的根本原因是过期文档物理上不在主分片可扫描范围内;需确保TTL字段为分片键最左前缀、类型为BSON Date、索引方向为升序且expireAfterSeconds≥3600。

在MongoDB分片集群中配置TTL索引后文档长期不删除,根本原因不是索引没建,而是过期文档物理上不在能被扫描的分片上——主分片无法定位到它们,清理线程自然跳过。
确认TTL字段是否参与分片键
进入mongosh,执行db.collection.getShardDistribution(),观察分片键结构。若输出中shard key不含时间字段(如createdAt),或仅含_id: "hashed",则TTL必然失效。
必须将TTL字段作为分片键的**最左前缀**或复合键的一部分,例如{ tenantId: 1, createdAt: 1 };【不能用{ _id: "hashed" }配TTL】,哈希打散后时间局部性彻底消失,扫描等同于全广播。
若当前分片键为{ _id: "hashed" },需重建集合:导出数据→删除原集合→用新分片键重新shardCollection→导入。
创建TTL索引的三个硬性条件
方法一:字段类型必须是BSON Date
用db.collection.findOne({ createdAt: { $exists: true } })检查返回值中createdAt是否显示为ISODate("...")。若显示为字符串("2026-09-01T08:00:00Z")或数字,则TTL完全不触发——MongoDB不会自动转换类型。
方法二:索引方向必须为升序(1)
执行db.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })。若误写为{ createdAt: -1 },命令静默成功但索引无效,getIndexes()里会显示"expireAfterSeconds": null。
方法三:expireAfterSeconds值不能小于3600
设为60会导致后台线程每秒唤醒却扫不到新过期文档,徒增CPU抖动;【生产环境必须≥3600】,这是WiredTiger引擎的最小安全阈值。
验证TTL是否真实生效
第一步:确认索引已挂载
运行db.collection.getIndexes(),找到对应索引项,检查是否同时存在"key": { "createdAt": 1 }和"expireAfterSeconds": 3600字段。
第二步:检查字段值覆盖率
执行db.collection.countDocuments({ createdAt: { $type: "date" } }),结果必须等于db.collection.estimatedDocumentCount()。若有差值,说明存在null、字符串或缺失值的文档,TTL线程会跳过它们且不报错。
第三步:模拟过期并观察日志
插入一条测试文档:db.collection.insertOne({ createdAt: new Date(Date.now() - 7200), data: "test" })。等待2分钟后,开启详细日志:db.setLogLevel(2, { component: { storage: { ttl: 2 } } }),然后查db.adminCommand({ getLog: "global" }),搜索Removing expired documents。若无此记录,说明扫描未触发或分片路由失败。

















