根本原因是MongoDB Plan Cache复用过时查询计划;首次按query shape试运行选最优索引并缓存,但数据分布变化后原“最优”变“最差”;Index Filter可强制指定索引绕过缓存。

当同一类查询在未修改代码、未重建索引的情况下,昨天毫秒级返回、今天却扫描全表耗时数秒,根本原因不是索引不存在,而是MongoDB查询计划器基于过时的评估结果复用了失效的执行计划。
Plan Cache机制导致“历史最优”变“当前最差”
MongoDB首次执行某query shape(字段名、操作符、嵌套结构完全一致)时,会让所有候选索引各自试运行——默认取前101条结果或扫到数据末尾,再按实际消耗(works值、文档检查数)打分,得分最高的plan写入Plan Cache。后续同shape查询直接复用该plan。
这一步操作起来很简单,直接复用缓存就行;但隐患在于:试运行时的参数值若不具代表性,比如首次查的是占比仅0.1%的status: "cancelled",优化器就会认定{status: 1}索引最优,后续查占比80%的status: "paid"也强制走该索引,造成大量无效扫描。
数据分布迁移、集合规模突增、历史归档后总量锐减,都会让当初缓存的“最优”变成当下“最差”。【Plan Cache不会自动感知数据变化,也不会主动重新评估】。
Index Filter是唯一能绕过Plan Cache的硬约束
Index Filter不依赖任何缓存,它是为特定query shape人工指定必须使用的索引,生效后不受clearPlanCache、实例重启、主备切换影响。
方法一:确认目标索引名
执行db.collection.getIndexes(),记下要锁定的索引字段顺序和方向,例如{"Tags.SN": 1, "OPTime": -1}。
方法二:构造精确匹配的query对象
只保留WHERE条件部分,去掉sort、projection、hint等任何附加操作;字段名大小写、$eq显式写出与否、嵌套层级都必须与真实查询完全一致。
方法三:下发filter指令
执行db.runCommand({ setIndexFilter: "collectionName", query: {"Tags.SN": "QZ435698245"}, indexes: [{"Tags.SN": 1, "OPTime": -1}] })。【indexes数组中必须包含当前query能合法使用的索引,否则直接报错"no matching index"】
验证Index Filter是否真正生效
第一步:执行带executionStats的explaindb.collection.find({"Tags.SN": "QZ435698245"}).explain("executionStats")
第二步:检查queryPlanner.indexFilterSet字段
若值为true,说明filter已加载;若为false,说明未命中query shape或命令执行失败。
第三步:确认executionStats.executionStages.stage确实是IXSCAN
且executionStats.totalDocsExamined远小于totalDocsAvailable——这证明索引真的被用了,不是explain里写着IXSCAN实际却COLLSCAN。











