MongoDB 的 explain() 方法会把一次查询在内部如何执行完整呈现出来,包括优化器挑选了哪个索引、扫描了多少条索引键、回表读取了多少文档、每个执行阶段消耗了多少时间。对于正在排查慢查询或设计索引的开发者来说,执行计划不是可有可无的辅助信息,而是判断查询是否真正高效的核心依据。只看查询响应时间往往会掩盖问题,比如一个查询在数据量小时很快,但执行计划已经暴露出集合扫描的行为,一旦数据增长就会迅速劣化。因此,学会从 explain 输出中提取关键信号,是 MongoDB 性能调优的基本功。

explain 的三种模式与调用方式
MongoDB 为 explain() 提供了三种详细程度不同的输出模式,分别是 queryPlanner、executionStats 和 allPlansExecution。queryPlanner 只展示查询优化器最终选择的执行计划以及被拒绝的候选计划,不包含实际执行后的统计信息;executionStats 在 queryPlanner 的基础上增加了执行阶段的统计结果,例如扫描的索引键数量、返回的文档数量、执行耗时等,是日常定位慢查询时最常用的模式;allPlansExecution 则进一步展示所有候选计划在执行阶段的统计信息,适合用来分析优化器在多索引竞争时为何做出某个选择。
调用方式非常简单,只需在 find() 或 aggregate() 的游标上追加 explain() 方法即可。下面的代码演示了以 executionStats 模式查看一个按年龄范围查询的执行计划:
db.users.find({ age: { $gt: 25, $lt: 40 } }).explain("executionStats");
如果只想查看默认的 queryPlanner 计划,可以不传参数或传入 "queryPlanner"。需要注意的是,explain() 不会真正返回查询结果,它执行的是计划分析而不是数据读取,因此可以在生产环境安全使用,但 executionStats 和 allPlansExecution 模式会实际执行查询的索引扫描与文档读取过程,如果查询本身代价很高,仍然会消耗一定资源,执行时需要酌情考虑。
除了 find 查询,聚合管道同样支持 explain()。例如对一个包含 $match 和 $group 的管道执行 explain,可以观察到管道各阶段如何拆分执行、索引是否在 $match 阶段被命中。这比单独观察最终结果更能发现聚合框架中的性能瓶颈。
executionStats 核心字段解读
拿到 executionStats 输出后,先要关注最顶层的几个指标:nReturned 表示查询最终返回给客户端的文档数量;totalKeysExamined 表示在索引中扫描的索引键个数;totalDocsExamined 表示在存储层实际读取的文档个数。如果 totalDocsExamined 远大于 nReturned,说明查询虽然用到了索引,但大量文档读取后被过滤掉了,通常意味着索引选择性不足或者查询条件与索引字段不匹配。同理,如果 totalKeysExamined 远大于 nReturned,也可能存在索引范围过大或排序带来的额外扫描。
一个典型的 executionStats 输出片段如下:
{
"executionStages": {
"stage": "FETCH",
"nReturned": 18,
"totalKeysExamined": 20,
"totalDocsExamined": 18,
"executionTimeMillis": 4,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 18,
"totalKeysExamined": 20,
"totalDocsExamined": 0,
"indexName": "age_1",
"direction": "forward"
}
}
}
上述输出中,外层 stage 为 FETCH,表示先通过内层 IXSCAN 扫描 age_1 索引,再根据索引指针回表读取完整文档。因为 totalKeysExamined 为 20,nReturned 为 18,差距很小,说明索引命中良好,只有极少量无效扫描。如果 stage 直接显示 COLLSCAN,则表示优化器没有选择任何索引,对集合进行了全表扫描,这通常是需要优先优化的危险信号。
另一个重要字段是 executionTimeMillis,它表示当前阶段的执行耗时。需要注意的是,在多阶段嵌套执行计划中,父阶段的耗时包含子阶段耗时,不应简单相加。更可靠的是观察各阶段的 nReturned 与扫描数量之间的比值,以及是否存在 SORT 阶段。如果执行计划中出现 SORT 阶段,说明查询结果在内存中进行了排序,当结果集较大时可能导致内存压力甚至报错,此时应考虑让索引顺序覆盖排序字段,以避免额外排序。
通过执行计划定位性能问题
执行计划最常见的用途是发现并消除集合扫描。集合扫描在 JSON 输出中表现为 stage 字段值为 COLLSCAN。以下示例展示了一个没有索引的查询:
{
"executionStages": {
"stage": "COLLSCAN",
"nReturned": 1,
"totalDocsExamined": 100000,
"executionTimeMillis": 120
}
}
这个查询只返回了 1 条文档,却扫描了 10 万条文档,执行耗时 120 毫秒。随着集合数据量增长,COLLSCAN 的耗时几乎会线性上升。解决方式通常是为查询条件中的字段创建合适的索引。例如上述查询如果基于 email 字段查找用户,可以创建唯一索引:
db.users.createIndex({ email: 1 }, { unique: true });
再次执行 explain 后,执行计划的 stage 会变为 IXSCAN,totalDocsExamined 会下降到与 nReturned 相同或相近的水平,查询性能会得到数量级提升。
除了 COLLSCAN,执行计划还能帮助判断复合索引的字段顺序是否合理。例如有一个查询条件为 { status: "active", age: { $gt: 25 } },如果创建的索引是 { age: 1, status: 1 },虽然优化器可能仍然使用该索引,但它只能利用 age 字段确定范围,随后在扫描到的多个 age 索引键中逐个过滤 status,导致 totalKeysExamined 偏高。如果调整索引为 { status: 1, age: 1 },前缀等值条件会先缩小范围,再使用 age 做范围扫描,totalKeysExamined 会明显下降。通过对比不同索引字段顺序下 explain 输出中的 totalKeysExamined 和 totalDocsExamined,可以验证索引设计是否真正匹配查询模式。
覆盖查询与索引排序的验证
覆盖查询是指查询需要的所有字段都包含在索引中,执行计划不会出现 FETCH 阶段,而是直接通过 IXSCAN 返回结果。覆盖查询的典型标志是 totalDocsExamined 为 0,并且执行阶段只有 IXSCAN 而没有 FETCH。例如,为查询 { status: "active" } 且只投映 { _id: 0, status: 1 } 的场景创建索引 { status: 1 },执行计划会显示 stage 为 IXSCAN,并且 totalDocsExamined 为 0。这意味着 MongoDB 无需回到集合中读取文档,读取路径大幅缩短,尤其适合高并发读场景。
排序操作也可以借助索引避免额外的 SORT 阶段。如果查询带有 sort({ created_at: -1 }),并且索引字段顺序包含 created_at,那么执行计划中通常不会出现 SORT 阶段,而是直接由 IXSCAN 按索引顺序返回结果。反之,如果执行计划中出现了 SORT 阶段,则应检查索引字段顺序与排序方向是否一致,必要时调整索引或重写查询。explain 输出中的 inputStage 会展示排序前的扫描方式,通过对比不同索引方案下的执行计划,可以找到让查询和排序同时走索引的最优解。
总之,MongoDB 的 explain 执行计划并不是只给 DBA 看的复杂元数据,而是每个后端开发都应该掌握的排查工具。无论是定位慢查询、验证索引有效性,还是优化聚合管道,学会读懂 stage、扫描数量和执行时间之间的关系,都能让性能调优工作变得有据可依,而不是盲目试错。