MongoDB 索引未被使用?用 explain() 和 $indexStats 一步步排查

主题: mongodb-index-not-used-explain-analysis更新于: 2026/6/24作者:AgentFactory 技术团队

当你的 MongoDB 查询越来越慢,而你已经创建了看似合适的索引,却发现查询计划中显示的是 COLLSCAN(集合扫描)而不是 IXSCAN(索引扫描),这通常意味着索引没有被正确使用。本文将通过具体的命令、输出分析和排查步骤,帮你定位索引未被使用的根因,并给出可操作的解决方案。

它解决什么问题 / 适用场景

这套基于 MongoDB 原生命令(explain()$indexStatshint())的分析方法,专门用于解决以下场景:

  • 慢查询排查:某个查询响应时间从毫秒级飙升到秒级,怀疑索引失效。
  • 索引策略验证:在创建新索引后,确认它是否被查询优化器选中。
  • 查询计划分析:理解 MongoDB 如何选择执行计划,以及为什么选择了非预期的索引(或没有索引)。
  • 生产环境变更评估:在修改索引或查询前,通过 explain() 模拟执行,评估性能影响。

这套方法不依赖任何第三方工具,只需 mongosh 或 MongoDB 驱动即可执行,非常适合在受限环境或自动化脚本中集成。

核心命令与参数说明

以下三个命令是分析索引使用情况的核心工具。建议在 mongosh 或你的应用代码中按需使用。

命令 / 方法用途关键参数输出重点
db.collection.explain("executionStats").find(...)返回查询的执行计划及详细统计mode"executionStats"(推荐)或 "allPlansExecution"stage(IXSCAN 或 COLLSCAN)、totalDocsExaminednReturnedexecutionTimeMillis
db.collection.aggregate([{ $indexStats: {} }])返回集合中所有索引的访问统计name(索引名)、accesses.ops(访问次数)
db.collection.find(...).hint({ index_key: 1 })强制查询使用指定索引index_specification:索引键模式(如 { zipcode: 1 })或 { $natural: 1 }(强制不使用索引)配合 explain() 可验证强制索引的效果

参数详解

  • modeexplain() 的详细模式。"executionStats" 返回实际执行的统计信息(如扫描文档数、执行时间),是日常排查的首选。"allPlansExecution" 还会返回在计划选择阶段被淘汰的其他计划的统计,适合分析优化器为何没选某个索引。
  • index_specificationhint() 的参数。可以是一个具体的索引键模式(如 { status: 1, created_at: -1 }),也可以是 { $natural: 1 } 来强制进行集合扫描(用于对比基线)。

索引未被使用的排查步骤

假设你有一个 orders 集合,查询 db.orders.find({ status: "shipped", created_at: { $gte: ISODate("2024-01-01") } }) 很慢,并且你已经创建了索引 { status: 1, created_at: -1 },但查询仍然慢。以下是排查流程。

步骤 1:确认索引是否存在且被统计

首先,检查索引是否真的存在,以及它是否被 MongoDB 的查询优化器考虑过。

JAVASCRIPT
// 查看集合的所有索引
db.orders.getIndexes()

// 查看索引的访问统计
db.orders.aggregate([{ $indexStats: {} }])

输出示例

JSON
{ "name": "status_1_created_at_-1", "accesses": { "ops": NumberLong(0) } }

如果 accesses.ops 为 0 或长期不增长,说明该索引从未被使用。这可能是索引创建后没有查询匹配,或者优化器选择了其他计划。

步骤 2:获取当前查询的执行计划

使用 explain("executionStats") 查看 MongoDB 实际是如何执行这个查询的。

JAVASCRIPT
db.orders.explain("executionStats").find(
  { status: "shipped", created_at: { $gte: ISODate("2024-01-01") } }
)

关键输出字段(重点关注 queryPlannerexecutionStats 部分):

JSON
"queryPlanner": {
  "winningPlan": {
    "stage": "COLLSCAN",   // ← 这里如果是 COLLSCAN,说明没有使用索引
    "filter": {
      "$and": [ { "status": { "$eq": "shipped" } }, { "created_at": { "$gte": ... } } ]
    }
  }
},
"executionStats": {
  "nReturned": 5000,
  "totalDocsExamined": 100000,   // ← 远大于 nReturned,说明扫描了大量文档
  "executionTimeMillis": 1200
}

根因分析

  • 如果 stageCOLLSCAN,说明 MongoDB 选择了全表扫描。常见原因包括:
    • 查询条件中的字段顺序与索引键顺序不匹配(例如,索引是 { status: 1, created_at: -1 },但查询只用了 created_at 字段)。
    • 查询使用了 $or$in$regex(非前缀)等可能阻止索引使用的操作符。
    • 索引选择性太差(例如,status 字段只有两个值,且查询匹配了大部分文档),优化器认为全表扫描更高效。
  • 如果 stageIXSCANtotalDocsExamined 远大于 nReturned,说明索引被使用了,但选择性差,需要优化索引或查询。

步骤 3:使用 hint() 强制验证索引效果

为了确认索引本身是否有效,可以强制查询使用该索引,并对比执行计划。

JAVASCRIPT
// 强制使用索引
db.orders.explain("executionStats").find(
  { status: "shipped", created_at: { $gte: ISODate("2024-01-01") } }
).hint({ status: 1, created_at: -1 })

对比输出

  • 如果强制使用索引后,stage 变为 IXSCAN,且 totalDocsExaminedexecutionTimeMillis 显著降低,说明索引本身是有效的,问题在于优化器没有选择它。
  • 如果强制使用索引后,totalDocsExamined 仍然很高,甚至比全表扫描还慢,说明索引设计有问题(例如,索引键顺序不合理,或索引无法有效过滤数据)。

步骤 4:修复与验证

根据步骤 3 的结果,采取相应措施:

现象根因解决方案
优化器未选索引,但强制索引有效查询条件与索引不匹配,或优化器误判1. 检查查询条件是否覆盖索引前缀字段。2. 使用 hint() 强制指定索引(仅限调试,生产环境慎用)。3. 考虑创建覆盖索引(covered query)。
强制索引后 totalDocsExamined 仍高索引选择性差1. 重新设计索引,例如增加更多过滤字段或调整字段顺序。2. 考虑使用复合索引,将高选择性的字段放在前面。
强制索引后执行时间反而更长索引本身效率低(如索引过大、更新频繁)1. 检查索引大小和内存占用。2. 考虑删除不必要的索引。3. 使用 $indexStats 监控索引使用频率。

常见报错与排查

以下是在使用这些命令时可能遇到的典型错误及解决方案。

错误 1:认证失败

MongoServerError: Authentication failed.

原因:MongoDB URI 中的用户名、密码或认证数据库(authSource)不正确。

解决

  1. 确认 URI 格式正确:mongodb://user:password@host:port/db?authSource=admin
  2. 如果使用 SCRAM-SHA-256,确保 MongoDB 版本(4.0+)支持。
  3. mongosh 中测试连接:mongosh "mongodb://user:password@host:port/admin"

错误 2:权限不足

MongoServerError: not authorized on admin to execute command { aggregate: "orders", pipeline: [ { $indexStats: {} } ] }

原因:用户缺少对目标数据库的 aggregateindexStats 权限。

解决: 在 admin 数据库中为用户授予必要权限:

JAVASCRIPT
use admin
db.grantRolesToUser("your_user", [
  { role: "readWrite", db: "your_database" }
])

或者创建自定义角色,仅授予 aggregateindexStats 操作权限。

错误 3:无查询解决方案

MongoServerError: PlanExecutor error: No query solution

原因:查询条件引用了不存在的字段,或使用了不支持的运算符(如对数组字段使用 $elemMatch 但索引不匹配)。

解决

  1. 检查查询 JSON 语法是否正确。
  2. 使用 db.collection.getIndexes() 确认现有索引。
  3. 简化查询条件,逐步排查是哪个条件导致问题。

常见问题 FAQ

Q: 如何判断一个索引是否被实际使用?

A: 使用 $indexStats 聚合阶段:

JAVASCRIPT
db.collection.aggregate([{ $indexStats: {} }])

返回结果中的 accesses.ops 字段表示该索引被访问的次数。如果该值为 0 或长期不增长,说明索引未被使用。注意:该统计仅在执行查询的节点上有效,分片集群需汇总所有分片数据。

Q: explain('executionStats') 中的 totalDocsExaminednReturned 有什么区别?

A: nReturned 是实际返回给客户端的文档数量,而 totalDocsExamined 是 MongoDB 为了找到这些文档而扫描的文档总数。理想情况下,两者应接近。如果 totalDocsExamined 远大于 nReturned,说明索引选择性差或查询条件未充分利用索引,需要优化索引或查询。

Q: 使用 hint() 强制指定索引后,如何验证其效果?

A: 在 hint() 后链式调用 explain('executionStats')

JAVASCRIPT
db.collection.find({...}).hint({index_key: 1}).explain('executionStats')

观察输出中的 stage 字段是否为 IXSCAN(索引扫描),以及 totalDocsExaminedexecutionTimeMillis 是否显著降低。对比不使用 hint() 时的执行计划,确认强制索引确实更优。

生产环境实践与注意事项

在生产环境中使用这些命令时,务必注意以下限制:

  1. 性能影响:频繁运行 explain("executionStats")$indexStats 会消耗数据库资源,尤其是在高并发写入场景下,可能导致查询延迟增加。建议在维护窗口或低峰期执行。
  2. 权限控制:需要用户具备对目标数据库的 aggregateexplain 权限,以及 system.indexes 集合的读取权限。最小权限原则下,应创建专用角色,避免授予 rootdbAdminAnyDatabase 等高权限角色。
  3. 分片集群注意事项$indexStats 在分片集群中仅返回当前节点数据,多节点并发执行可能导致统计不一致。建议在 mongos 上执行或汇总所有分片结果。
  4. 网络安全:MongoDB URI 中包含明文密码,建议使用环境变量或密钥管理服务(如 Vault)注入,避免硬编码。
  5. 数据一致性explain() 不实际执行查询,但会获取读锁。在副本集 secondary 节点上执行可能因复制延迟导致计划不准确。

相关深度解决方案

在配置当前服务时,如果您需要实现更复杂的架构或多源数据整合,建议配合参考我们整理的 Next.js App Router vs Pages Router 深度实战与迁移白皮书

在配置当前服务时,如果您需要实现更复杂的架构或多源数据整合,建议配合参考我们整理的 Argo CD 深度实战指南:GitOps 多集群部署、生产调优与故障排查白皮书