explain("executionStats")是唯一可信依据:nReturned≈totalKeysExamined且totalDocsExamined接近nReturned才代表索引高效命中;stage为IXSCAN但totalDocsExamined远大于nReturned说明索引过滤低效;聚合中仅最前端$match可能走集合索引。

explain("executionStats") 是唯一可信的索引命中判断依据
只用 explain() 或 explain("queryPlanner") 看到 IXSCAN 就以为索引命中,是最大误区。真正决定是否“有效命中”的,是 explain("executionStats") 里那三个数:nReturned、totalKeysExamined、totalDocsExamined。它们不撒谎——哪怕 winningPlan.stage 显示 IXSCAN,只要 totalDocsExamined 比 nReturned 大几十倍,说明索引只扫了键,还得回表逐个过滤文档,等于白建。
-
totalKeysExamined ≈ nReturned:索引过滤精准,基本没做无用功 -
totalDocsExamined ≫ nReturned(比如 100 万 vs 100):索引前缀没对齐,或范围查询太宽,导致大量 FETCH + filter -
stage === "COLLSCAN":连索引都没用上,优先检查字段是否拼错、类型是否不匹配(如字符串查成数字)
聚合管道里 $match 的索引是否生效,得看位置和 stage 路径
聚合中只有最前端的 $match 可能走集合索引;它后面插一个 $unwind 或 $lookup,再跟的 $match 就只扫内存中间结果,不触发索引也不更新 $indexStats。验证方法不是看有没有 IXSCAN,而是盯住 executionStats.executionStages 的嵌套结构:
- 如果第一层
stage是IXSCAN,且indexName是你预期的索引名 → 前端$match命中了 - 如果
executionStages里某层stage是IXSCAN,但它的父节点是UNWIND或SHARD_MERGE→ 这个 IXSCAN 扫的是中间数据,不算集合索引命中 -
totalKeysExamined接近nReturned,但totalDocsExamined极低 → 很可能走了覆盖索引(indexOnly: true),不用 FETCH
hint() 不是救命稻草,而是验证索引设计是否合理的探针
hint() 的作用从来不是“让慢查询变快”,而是帮你确认:“如果强制走这个索引,会不会比优化器选的更好?” 它暴露的是当前索引设计或查询写法的问题,而不是替代方案。
- 必须配合
explain("executionStats")使用,否则看不出耗时/扫描量变化 - 常见误用:对复合索引只传部分字段(如索引是
{"a": 1, "b": 1, "c": 1},却hint({"a": 1}))→ MongoDB 会拒绝并报错error: "hint not valid" - 聚合管道中
hint()只能加在最外层aggregate()后,不能塞进某个$match阶段里 - 如果
hint()强制后totalDocsExamined显著下降,说明优化器选错了计划;如果没变化,大概率是索引本身覆盖不足,该重排字段顺序了
别信 explainVersion 2 的 stage 名字,要顺藤摸瓜看 inputStage
MongoDB 6.0+ 默认用经典查询引擎,但输出里 stage 字段可能嵌套很深。比如看到 "stage": "FETCH",别急着骂“没走索引”——它的 inputStage 很可能是 IXSCAN,FETCH 只是取文档这一步。真瓶颈在上一层。
- 从根节点往叶子节点读:找最深的
IXSCAN或COLLSCAN,那是实际数据源 - 如果
IXSCAN下面跟着filter字段(不是indexBounds),说明索引没覆盖filter条件,得补字段 -
executionTimeMillisEstimate和executionTimeMillis差距大?说明缓存干扰严重,多跑几次取平均值更准
真实慢查询里,最常被忽略的是 totalDocsExamined 和 totalKeysExamined 的比值关系——它不体现在任何 UI 监控里,只能靠你手动翻 explain("executionStats") 的 JSON 树。











