5分钟 Get `MongoDB Explain` 分析小工具

2022-06-11  本文已影响0人  wn777

使用场景 / 基础用途

针对于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类似的分析工具。

上一篇下一篇

猜你喜欢

热点阅读