$indexStats 是唯一能反映索引“长期未用”的可靠指标,通过 accesses.ops 和 accesses.since 判断真实使用情况,需结合慢查询日志交叉验证伪活跃索引,并注意 id 索引不可删及部分索引匹配限制。

用 $indexStats 查 accesses.ops 和 accesses.since
直接执行聚合就能拿到索引使用计数和时间戳,这是唯一能反映“长期未用”的可靠指标:db.collection.aggregate([ {$indexStats: {}}, {$project: { name: 1, usageCount: "$accesses.ops", since: "$accesses.since" }} ])
关键看两列:
- usageCount 是自 mongod 启动以来该索引被真正用于查找/排序/范围扫描的次数,不是“被选中”次数
- since 是计数器重置时间,如果它很新(比如刚重启过),usageCount === 0 就不能立刻判定为僵尸索引
真正要警惕的是:usageCount 长期为 0,且 since 是几天甚至几周前——说明这段时间内业务查询压根没触达它
为什么不能只靠 explain() 判断索引是否被用
explain("executionStats") 只反映单次查询路径,完全看不到时间维度上的使用分布,常见误判包括:
- 开发环境跑一次 find({username: "a"}) 显示走了 username_1 索引,但线上这个查询根本没被执行过
- 聚合管道里 $match 放在 $unwind 后面,explain 显示用了索引,但那是对中间结果集扫描,$indexStats 对这种访问无感
- 监控脚本定期调用 db.collection.stats() 或 count(),会偶然触发 _id_ 索引,但不代表业务逻辑依赖它
识别 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 基本就是干扰项
别漏掉 _id_ 和部分索引的特殊性
_id_ 索引永远存在且不可删,它会出现在 getIndexes() 返回里,名字固定为 "_id_",别误删;
部分索引(含 partialFilterExpression)必须和查询谓词严格匹配才可能被选中,哪怕 accesses.ops === 0,也可能是查询条件没覆盖过滤表达式,而不是索引没用;
另外,$indexStats 不统计对视图或分片集合的索引访问,如果你用的是分片集群,得在每个分片上单独查

















