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

长期稳定更新的攒劲资源: >>>点此立即查看<<<
先看最基础的用法,直接调 explain() 就能拿到当前查询的执行计划,不指定模式的话默认走 queryPlanner。
示例:基本 explain 使用
db.students.find({ studentId: 12345 }).explain();
explain 命令支持三种模式,区别在于输出信息的详细程度:
示例:不同模式的 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");
拿到输出后,关键看哪几个字段?以下面这个 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"
}
}
namespace:数据库.集合名,一眼知道查的是哪张表。parsedQuery:MongoDB 解析后的查询条件,用来确认查询是否被正确转换。winningPlan:最终选中的执行计划,里面会暴露索引使用情况(比如这里用了 studentId_1 索引做 IXSCAN,然后 FETCH 回表取文档)。rejectedPlans:被优化器抛弃的备选计划,如果这里不为空,说明存在多个候选索引,值得关注。executionSuccess:查询是否正常执行完。nReturned:最终返回了多少条文档。executionTimeMillis:总执行时间(毫秒),优化前后可以直接对比这个数值。totalKeysExamined:扫描的索引键总数。理想情况下应该等于 nReturned,如果远大于 nReturned,说明索引选择性不好。totalDocsExamined:扫描的文档总数。越小越好,如果这个数很大而 nReturned 很小,很可能没有走索引或者索引不能覆盖查询。stage:每个步骤的名称,常见的包括 IXSCAN(索引扫描)、FETCH(回表取文档)、COLLSCAN(全表扫描,见它基本可以断定索引没用好)。nReturned:该步骤返回的文档数。executionTimeMillisEstimate:该步骤的估计耗时。keysExamined 和 docsExamined:该步骤扫描的索引键数和文档数,逐层累加。inputStage:下一级输入(子步骤),层层嵌套直到叶子节点。实际业务中往往不是单字段查询,比如要按姓氏 + 名字精确查找。假设已经建了一个复合索引 { 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 这两个数字——数字越小,数据库就越轻松。坚持这个习惯,查询性能的提升会非常直观。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述