5分钟 Get `MongoDB Explain` 分析小工具
使用场景 / 基础用途
针对于MongoDB中的慢查询,我们通常会使用explain命令进行分析,其结果包含MongoDB实际执行过程,但返回往往比较冗长,不够直观。
最近发现一款小工具,能更加便捷快速查看explain分析结果,和大家分享下。
先看效果,通过小工具,我们可以拿到如下结果,
mongo> mongoTuning.executionStats(explainDoc);
1 COLLSCAN ( ms:10427 docs:411121)
2 SORT_KEY_GENERATOR ( ms:10427)
3 SORT ( ms:10427)
4 PROJECTION_SIMPLE ( ms:10428)
Totals: ms: 12016 keys: 0 Docs: 411121
使用方法
1) 下载小工具
https://github.com/gharriso/MongoDBPerformanceTuningBook 中 mongoTuning.js,
2) 使用Mongo Shell的时候,引入
mongo --shell mongoTuning.js
3) 使用示例
假设我们有一段查询如下:
var explainCsr = db.customers.explain()
.find(
{
FirstName: "RUTH",
LastName: "MARTINEZ",
Phone: 496523103
},
{
Address: 1,
dob: 1
})
.sort({ dob: 1 });
a) 首先,var explainDoc = explainCsr.next(); 看下原始结果 printjson(explainDoc.queryPlanner.winningPlan);
, 如下,
mongo> printjson(explainDoc.queryPlanner.winningPlan);
{
"stage": "PROJECTION_SIMPLE",
"transformBy": {
"Address": 1,
"dob": 1
},
"inputStage": {
"stage": "SORT",
"sortPattern": {
"dob": 1
},
"inputStage": {
"stage": "SORT_KEY_GENERATOR",
"inputStage": {
"stage": "FETCH",
"filter": {
"$and": [ <
snip >
]
},
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"Phone": 1
},
"indexName": "Phone_1",
"isMultiKey": false,
"multiKeyPaths": {
"Phone": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"Phone": [
"[496523103.0, 496523103.0]"
]
}
}
}
}
}
}
b) 其次,看下分析工具给出结果 mongoTuning.quickExplain(explainDoc)
, 如下
Mongo Shell>mongoTuning.quickExplain(explainDoc)
1 IXSCAN Phone_1
2 FETCH
3 SORT_KEY_GENERATOR
4 SORT
5 PROJECTION_SIMPLE
是不是更加的简洁清晰。 这里 mongoTuning.quickExplain(explainDoc)
在实际的执行逻辑中,其实默认去查explainDoc.queryPlanner.winningPlan
(mongoTuning.quickExplain(explainDoc)
=mongoTuning.quickExplain(explainDoc.queryPlanner.winningPlan)
),
如果我们想看那些 rejectedPlan
, 则可以 mongoTuning.quickExplain(explainDoc.queryPlanner.rejectedPlans[1])
, 如下:
Mongo> mongoTuning.quickExplain (explainDoc.queryPlanner.rejectedPlans[1])
1 IXSCAN LastNmae_1
2 IXSCAN Phone_1
3 AND_SORTED
4 FETCH
5 SORT_KEY_GENERTOR
6 SORT
7 PROJECTION_SIMPLE
此外,如果想看一些每一步耗时
和 文档数量
, 可以使用explain('executionStats')
,:
var explainObj = db.customers.explain('executionStats')
.find(
{
FirstName: "RUTH",
LastName: "MARTINEZ",
Phone: 496523103
},
{
Address: 1,
dob: 1
}
)
.sort(
{ dob: 1 }
);
原始结果如下,
mongo> explainDoc.executionStats
{
"executionSuccess": true,
"nReturned": 1,
"executionTimeMillis": 0,
"totalKeysExamined": 1,
"totalDocsExamined": 1,
"executionStages": {
"stage": "PROJECTION_SIMPLE",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 6,
"advanced": 1,
"needTime": 3,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"transformBy": {
"Address": 1,
"inputStage": {
"stage": "SORT",
// Many, many more lines of output
}
}
}
}
带入到工具中,
mongo> mongoTuning.executionStats(explainDoc);
1 COLLSCAN ( ms:10427 docs:411121)
2 SORT_KEY_GENERATOR ( ms:10427)
3 SORT ( ms:10427)
4 PROJECTION_SIMPLE ( ms:10428)
Totals: ms: 12016 keys: 0 Docs: 411121
简单明了!
原理解析
使用介绍完成,如果感兴趣实现,我们看下分析工具是怎么运作的。
以 mongoTuning.quickExplain 举例
1) 入口代码
主体调用函数 prepExplain 和 printInputStage.
mongoTuning.quickExplain = (inputPlan) => {
// Takes as input an explain Plan. Emits a simplified
// version of that plan
const explainPlan = mongoTuning.prepExplain(inputPlan);
let stepNo = 1;
const printSpaces = function (n) {
let s = '';
for (let i = 1; i < n; i++) {
s += ' ';
}
return s;
};
const printInputStage = function (step, depth) {
if ('inputStage' in step) {
printInputStage(step.inputStage, depth + 1);
}
if ('inputStages' in step) {
step.inputStages.forEach((inputStage) => {
printInputStage(inputStage, depth + 1);
});
}
if ('indexName' in step) {
print(stepNo++, printSpaces(depth), step.stage, step.indexName);
} else {
print(stepNo++, printSpaces(depth), step.stage);
}
};
printInputStage(explainPlan, 1);
};
2) prepExplain是为了获取 explainPlan
, 代码
mongoTuning.prepExplain = (explainInput) => {
// Takes as input explain output in one of the follow formats:
// A fully explain JSON document, in which case emits winningPlan
// An explain() cursor in which case, extracts the winningPlan from the cursor
// A specific plan step in which case just returns that
const keys = Object.keys(explainInput);
// printjson(keys);
if (keys.includes('queryPlanner')) {
// This looks like a top level Explain
return explainInput.queryPlanner.winningPlan;
} else if (keys.includes('hasNext')) {
// This looks like a cursor
if (explainInput.hasNext()) {
return mongoTuning.prepExplain(explainInput.next());
}
return { ok: 0, error: 'No plan found' };
} else if (keys.includes('stage')) {
// This looks like an actual plan
return explainInput;
}
return { ok: 0, error: 'No plan found' };
};
可以看出,如果我们把之前的 explainDoc
传入, 会默认拿到其 queryPlanner.winningPlan
, 就是默认拿到winningPlan
3) printInputStage
在看 printInputStage 前,先看下 explainDoc.queryPlanner.winningPlan结构,
explainDoc.queryPlanner.winningPlan结构这里展示MongoDB执行过程中stage之间的关系,
(1) stage "PROJECTION_SIMPLE" 依赖 (2)stage "SORT",
(2) stage "SORT" inputStage 依赖 (3)stage "SORT_KEY_GENERATOR",
依次类推,直到(5)stage IXSCAN不再依赖 inputStage ,则结束。
所以正常的顺序是
Mongo Shell>mongoTuning.quickExplain(explainDoc)
1 IXSCAN Phone_1
2 FETCH
3 SORT_KEY_GENERATOR
4 SORT
5 PROJECTION_SIMPLE
如上则 printInputStage 对应这个递归调用过程,代码如下,有inputStage则继续找,直到找到没有inputStage的stage,输出step.stage
后返回。
const printInputStage = function (step, depth) {
if ('inputStage' in step) {
printInputStage(step.inputStage, depth + 1);
}
if ('inputStages' in step) {
step.inputStages.forEach((inputStage) => {
printInputStage(inputStage, depth + 1);
});
}
if ('indexName' in step) {
print(stepNo++, printSpaces(depth), step.stage, step.indexName);
} else {
print(stepNo++, printSpaces(depth), step.stage);
}
};
printInputStage(explainPlan, 1);
总结和延伸
通过分析工具,能更清晰简洁地拿到explain结果。
此外,由于 mongo --shell mongoTuning.js 支持js语法,我们可以根据需求定制的Mongo Shell命令,做出Explain类似的分析工具。