直接结论:用 $indexstats 聚合阶段查 accesses.ops 为 0 且 accesses.since 早于关心的时间窗口(如 7 天前),基本就是僵尸索引;因 explain() 仅反映单次查询路径,无法体现时间维度使用分布,易误判开发环境测试、$unwind 后 $match、监控脚本触发等伪活跃情况。

直接结论:用 $indexStats 聚合阶段查 accesses.ops,值为 0 且 accesses.since 时间早于你关心的时间窗口(比如 7 天前),基本就是僵尸索引。
为什么不能只靠 explain() 判断索引是否被用
单次 explain("executionStats") 只反映那一条查询的执行路径,完全看不到时间维度上的使用分布。常见误判包括:
- 开发环境跑一次
find({username: "a"})显示走了username_1索引,但线上这个查询压根没被执行过 - 聚合管道里
$match放在$unwind后面,explain显示用了索引,但那是对中间结果集扫描,$indexStats对这种访问无感——它只统计对原始集合的索引命中 - 监控脚本定期调用
db.collection.stats()或count(),会偶然触发_id_索引,但不代表业务逻辑依赖它
怎么用 $indexStats 快速筛出长期未用的索引
执行以下命令(替换 collection 为你的集合名):
db.collection.aggregate([
{ $indexStats: {} },
{ $project: {
name: 1,
usageCount: "$accesses.ops",
lastUsed: "$accesses.last",
since: "$accesses.since"
}
}
])
关键看三列:
-
name:索引名,注意_id_是默认索引,别误删 -
usageCount:自 mongod 启动以来该索引被真正用于查找/排序/范围扫描的次数,不是“被选中”次数 -
since:计数器重置时间,如果这个时间很新(比如刚重启过),usageCount === 0就不能立刻下结论
真正危险的是:usageCount 长期为 0,且 since 是几天甚至几周前——说明这段时间内它确实没被任何业务查询触达。
识别“伪活跃”索引:accesses.ops > 0 但实际无价值
有些索引虽然 accesses.ops > 0,但只是噪音,不是业务支撑索引:
- 只被
count()、distinct()这类元操作触发,而非find或aggregate中的主查询条件 - 被慢查询日志里出现频率极低的调试语句偶然触发(例如
find({ _id: ObjectId("...") })走了_id_,但业务代码里根本不用这种查法) - 被定时监控脚本带动,比如每分钟调一次
db.collection.stats()
这时要交叉验证:开启慢查询日志 db.setProfilingLevel(1, { slowms: 100 }),再查 db.system.profile.find({ "indexName": "your_index_name" }).pretty()。如果 profile 里完全找不到该索引名,那它的 ops 基本就是干扰项。
删除前必须确认的三个动作
发现某个索引 accesses.ops === 0 并不等于可以立刻删:
- 检查是否有定时任务、ETL 脚本或离线分析作业在非高峰时段(比如凌晨 2 点)使用它——这类查询不会出现在白天流量里,但
$indexStats会如实记录 - 确认它不是某个复合索引的“前缀子集”,比如存在
{ a: 1, b: -1 }和{ a: 1 },删掉后者前得验证业务里是否存在大量只查a且不要排序的轻量查询 - 生产环境删之前,先在从节点或备份库上执行
db.collection.dropIndex("index_name")验证语法和权限,避免误删主键或唯一约束索引
最常被忽略的一点:索引删除是不可逆的写操作,且会阻塞后续写入——务必避开业务高峰,且删完立刻用 db.collection.getIndexes() 确认结果。











