MongoDB explain命令使用步骤与代码示例
在实际的数据库调优工作中,explain 命令可以说是最趁手的诊断工具。它能帮你把 MongoDB 查询的执行计划完整地摊开在桌面上,哪一步走了索引、哪一步走了全表扫描、到底花了多少时间,一目了然。下面通过几个具体的场景,把 explain 的用法和输出解读彻底说清楚。 1. 基本使用 先看最基础的
在实际的数据库调优工作中,explain 命令可以说是最趁手的诊断工具。它能帮你把 MongoDB 查询的执行计划完整地摊开在桌面上,哪一步走了索引、哪一步走了全表扫描、到底花了多少时间,一目了然。下面通过几个具体的场景,把 explain 的用法和输出解读彻底说清楚。

1. 基本使用
先看最基础的用法,直接调 explain() 就能拿到当前查询的执行计划,不指定模式的话默认走 queryPlanner。
示例:基本 explain 使用
db.students.find({ studentId: 12345 }).explain();
2. explain() 的三种模式
explain 命令支持三种模式,区别在于输出信息的详细程度:
- queryPlanner:只返回查询的逻辑计划和索引使用情况,不真正执行查询,轻量快捷。
- executionStats:返回逻辑计划 + 索引使用情况 + 实际执行统计(扫描了多少文档、花了多少毫秒),最常用。
- allPlansExecution:返回所有备选计划的执行统计,适合对比不同索引策略的性能。
示例:不同模式的 explain
// queryPlanner 模式
db.students.find({ studentId: 12345 }).explain("queryPlanner");
// executionStats 模式(推荐)
db.students.find({ studentId: 12345 }).explain("executionStats");
// allPlansExecution 模式
db.students.find({ studentId: 12345 }).explain("allPlansExecution");
3. explain() 输出解读
拿到输出后,关键看哪几个字段?以下面这个 executionStats 模式的输出为例,逐一拆解。
示例输出(executionStats 模式)
{
"queryPlanner": {
"plannerVersion": 1,
"namespace": "school.students",
"indexFilterSet": false,
"parsedQuery": { "studentId": { "$eq": 12345 } },
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "studentId": 1 },
"indexName": "studentId_1",
"direction": "forward",
"indexBounds": { "studentId": [ "[12345, 12345]" ] }
}
},
"rejectedPlans": []
},
"executionStats": {
"executionSuccess": true,
"nReturned": 1,
"executionTimeMillis": 2,
"totalKeysExamined": 1,
"totalDocsExamined": 1,
"executionStages": {
"stage": "FETCH",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"sa veState": 0,
"restoreState": 0,
"isEOF": 1,
"invalidates": 0,
"docsExamined": 1,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"sa veState": 0,
"restoreState": 0,
"isEOF": 1,
"invalidates": 0,
"keyPattern": { "studentId": 1 },
"indexName": "studentId_1",
"isMultiKey": false,
"multiKeyPaths": { "studentId": [] },
"indexBounds": { "studentId": [ "[12345, 12345]" ] },
"keysExamined": 1,
"seeks": 1,
"dupsTested": 0,
"dupsDropped": 0
}
}
},
"serverInfo": {
"host": "localhost",
"port": 27017,
"version": "4.4.6",
"gitVersion": "22c124145fa3bfdaeafb3f6d1b5f3d4e8391fe86"
}
}
关键字段解读
- queryPlanner 部分:
namespace:数据库.集合名,一眼知道查的是哪张表。parsedQuery:MongoDB 解析后的查询条件,用来确认查询是否被正确转换。winningPlan:最终选中的执行计划,里面会暴露索引使用情况(比如这里用了studentId_1索引做IXSCAN,然后FETCH回表取文档)。rejectedPlans:被优化器抛弃的备选计划,如果这里不为空,说明存在多个候选索引,值得关注。
- executionStats 部分:
executionSuccess:查询是否正常执行完。nReturned:最终返回了多少条文档。executionTimeMillis:总执行时间(毫秒),优化前后可以直接对比这个数值。totalKeysExamined:扫描的索引键总数。理想情况下应该等于nReturned,如果远大于nReturned,说明索引选择性不好。totalDocsExamined:扫描的文档总数。越小越好,如果这个数很大而nReturned很小,很可能没有走索引或者索引不能覆盖查询。
- executionStages 部分(执行计划的树形结构):
stage:每个步骤的名称,常见的包括IXSCAN(索引扫描)、FETCH(回表取文档)、COLLSCAN(全表扫描,见它基本可以断定索引没用好)。nReturned:该步骤返回的文档数。executionTimeMillisEstimate:该步骤的估计耗时。keysExamined和docsExamined:该步骤扫描的索引键数和文档数,逐层累加。inputStage:下一级输入(子步骤),层层嵌套直到叶子节点。
4. 示例:复合索引和多条件查询
实际业务中往往不是单字段查询,比如要按姓氏 + 名字精确查找。假设已经建了一个复合索引 { lastName: 1, firstName: 1 },看看 explain 怎么反馈。
创建复合索引
db.students.createIndex({ lastName: 1, firstName: 1 });
查询及执行计划分析
db.students.find({ lastName: "Smith", firstName: "John" }).explain("executionStats");
示例输出及解读
{
"queryPlanner": {
"plannerVersion": 1,
"namespace": "school.students",
"indexFilterSet": false,
"parsedQuery": { "lastName": { "$eq": "Smith" }, "firstName": { "$eq": "John" } },
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "lastName": 1, "firstName": 1 },
"indexName": "lastName_1_firstName_1",
"direction": "forward",
"indexBounds": {
"lastName": [ "["Smith", "Smith"]" ],
"firstName": [ "["John", "John"]" ]
}
}
},
"rejectedPlans": []
},
"executionStats": {
"executionSuccess": true,
"nReturned": 1,
"executionTimeMillis": 1,
"totalKeysExamined": 1,
"totalDocsExamined": 1,
"executionStages": {
"stage": "FETCH",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"sa veState": 0,
"restoreState": 0,
"isEOF": 1,
"invalidates": 0,
"docsExamined": 1,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"sa veState": 0,
"restoreState": 0,
"isEOF": 1,
"invalidates": 0,
"keyPattern": { "lastName": 1, "firstName": 1 },
"indexName": "lastName_1_firstName_1",
"isMultiKey": false,
"multiKeyPaths": { "lastName": [], "firstName": [] },
"indexBounds": {
"lastName": [ "["Smith", "Smith"]" ],
"firstName": [ "["John", "John"]" ]
},
"keysExamined": 1,
"seeks": 1,
"dupsTested": 0,
"dupsDropped": 0
}
}
}
}
从输出可以看到,复合索引被完美利用:先按 lastName 精确匹配,再按 firstName 精确匹配,totalKeysExamined 和 totalDocsExamined 都只扫了 1 条,效率极高。如果 totalDocsExamined 远大于 nReturned,通常说明索引没有覆盖查询条件,或者查询字段顺序与索引字段顺序不匹配。
用好 explain,就等于给查询做了一次“X光检查”。每次建完索引、调完查询后,顺手跑一遍 explain("executionStats"),重点看 totalKeysExamined 和 totalDocsExamined 这两个数字——数字越小,数据库就越轻松。坚持这个习惯,查询性能的提升会非常直观。
