不能直接依赖$indexStats的accesses.ops判断索引是否可删——它重启清零、不统计写路径隐式使用、也不反映约束类索引真实价值;执行必须指定集合名且仅限当前库,不支持跨库或mongos全局视图;unique、TTL、partial索引即使ops为0也须人工保留;需结合explain("executionStats")交叉验证索引实际使用效果,并以UTC时间比对accesses.since,辅以代码与业务逻辑确认冗余性。

不能直接依赖 $indexStats 的 accesses.ops 值判断索引是否“可删”——它重启清零、不统计写路径隐式使用、也不反映约束类索引的真实价值。
执行 $indexStats 聚合必须指定集合名且只作用于当前数据库
命令格式固定,不能省略集合名,也不能跨库扫描:
db.orders.aggregate([ { $indexStats: {} } ])
常见错误包括:
- 误写成
db.aggregate([{$indexStats:{}}])→ 报错no collection specified - 在 admin 数据库下运行却想查
mydb.users→ 返回空结果,因为$indexStats不支持db.getSiblingDB()或跨库引用 - 对分片集群的 mongos 执行 → 只返回该 mongos 连接的主分片(shard)上的统计,非全局视图
若需全量采集,得在每个 shard 的 primary 节点上分别执行,并手动合并结果。
accesses.ops === 0 不等于“没用”,要过滤三类关键索引
零访问量最危险的误操作就是直接删索引。以下三类必须人工保留并验证业务逻辑:
-
"unique": true的索引:删掉后插入重复值会报错,但更新时可能跳过校验(取决于 write concern),导致数据不一致 -
"expireAfterSeconds"字段存在的 TTL 索引:即使ops为 0,后台线程仍每 60 秒扫描一次过期文档 -
"partialFilterExpression"定义的稀疏索引:业务代码可能只在特定条件分支中触发它,$indexStats捕捉不到低频路径
检查方式是先跑 db.orders.getIndexes(),再逐条比对 $indexStats 输出中的 name 字段,把带上述选项的索引名从待清理列表里剔除。
结合 explain("executionStats") 验证索引是否真被选中
$indexStats 是宏观统计,explain() 是微观快照。两者必须交叉验证:
- 对高频查询执行
db.orders.find({status:"paid"}).explain("executionStats"),重点看executionStats.nReturned和executionStats.totalDocsExamined是否接近;若后者远大于前者,说明索引未生效或选择性差 - 若
queryPlanner.indexBounds为空或显示{},代表查询根本没走任何索引,此时$indexStats里对应索引的ops自然不会增长 - 用
hint()强制走某索引再explain(),例如db.orders.find({a:1,b:2}).hint({a:1,b:1}).explain("executionStats"),能确认该索引在当前查询结构下是否具备实际服务能力
注意:executionStats 中的 executionTimeMillis 单独看没意义,必须和 totalDocsExamined 一起算单位文档处理耗时(ms/doc),才能横向比较不同索引效率。
accesses.since 是 UTC 时间戳,别用本地时间比对
accesses.since 字段值形如 ISODate("2025-03-12T08:45:22.123Z"),是服务器 UTC 时间。运维同学常犯的错是拿自己电脑的北京时间(UTC+8)去比对,误判“半年没访问”。
正确做法是:
- 用
new Date().toUTCString()在 mongosh 里生成当前 UTC 时间做参照 - 若
since早于 180 天前的 UTC 时间,且ops < 5(不是 0),才进入候选池 - 对候选索引,必须翻代码确认是否有定时任务、CDC 工具或聚合管道里的
$lookup依赖它——这些调用可能每天只跑一次,ops增长极慢但不可或缺
真正难的不是发现冷索引,而是证明它“在当前业务上下文中确实冗余”。$indexStats 只提供线索,不是判决书。

















