MongoDB干货系列2-MongoDB执行计划分析详解(2)

写在之前的话

作为近年最为火热的文档型数据库,MongoDB受到了越来越多人的关注,但是由于国内的MongoDB相关技术分享屈指可数,不少朋友向我抱怨无从下手。

《MongoDB干货系列》将从实际应用的角度来进行MongoDB的一些列干货的分享,将覆盖调优,troubleshooting等方面,希望能对大家带来帮助。

如果希望了解更多MongoDB基础的信息,还请大家Google下。

要保证数据库处于高效、稳定的状态,除了良好的硬件基础、高效高可用的数据库架构、贴合业务的数据模型之外,高效的查询语句也是不可少的。那么,如何查看并判断我们的执行计划呢?我们今天就来谈论下MongoDB的执行计划分析。

引子

MongoDB 3.0之后,explain的返回与使用方法与之前版本有了不少变化,介于3.0之后的优秀特色,本文仅针对MongoDB 3.0+的explain进行讨论。

现版本explain有三种模式,分别如下:

  • queryPlanner

  • executionStats

  • allPlansExecution

由于文章字数原因,本系列将分为三个部分。
第一部分
第二部分
第三部分
本文是第二部分,主要是对IndexFilterStage进行分析。

正文

IndexFilter

IndexFilter决定了查询优化器对于某一类型的查询将如何使用index,indexFilter仅影响查询优化器对于该类查询可以用尝试哪些index的执行计划分析,查询优化器还是根据分析情况选择最优计划。

如果某一类型的查询设定了IndexFilter,那么执行时通过hint指定了其他的index,查询优化器将会忽略hint所设置index,仍然使用indexfilter中设定的查询计划。

IndexFilter可以通过命令移除,也将在实例重启后清空。

IndexFilter的创建

可以通过如下命令为某一个collection建立indexFilter

db.runCommand(
   {
      planCacheSetFilter: <collection>,
      query: <query>,
      sort: <sort>,
      projection: <projection>,
      indexes: [ <index1>, <index2>, ...]
   }
)
db.runCommand(
   {
      planCacheSetFilter: "orders",
      query: { status: "A" },
      indexes: [
         { cust_id: 1, status: 1 },
         { status: 1, order_date: -1 }
      ]
   }
)

上图针对orders表建立了一个indexFilter,indexFilter指定了对于orders表只有status条件(仅对status进行查询,无sort等)的查询的indexes,故下图的查询语句的查询优化器仅仅会从{cust_id:1,status:1}{status:1,order_date:-1}中进行winning plan的选择

db.orders.find( { status: "D" } )
db.orders.find( { status: "P" } )
indexFilter的列表

可以通过如下命令展示某一个collecton的所有indexFilter

db.runCommand( { planCacheListFilters: <collection> } )
indexFilter的删除

可以通过如下命令对IndexFilter进行删除

db.runCommand(
   {
      planCacheClearFilters: <collection>,
      query: <query pattern>,
      sort: <sort specification>,
      projection: <projection specification>
   }
)

Stage的意义

explain.queryPlanner.winningPlan.stageexplain.queryPlanner.winningPlan.inputStage等。

文档中仅有如下几类介绍

COLLSCAN

全表扫描

IXSCAN

索引扫描

FETCH

根据索引去检索指定document

SHARD_MERGE

将各个分片返回数据进行merge

但是根据源码中的信息,个人还总结了文档中没有的如下几类(常用如下,由于是通过源码查找,可能有所遗漏)

SORT

表明在内存中进行了排序(与老版本的scanAndOrder:true一致)

LIMIT

使用limit限制返回数

SKIP

使用skip进行跳过

IDHACK

针对_id进行查询

SHARDING_FILTER

通过mongos对分片数据进行查询

COUNT

利用db.coll.explain().count()之类进行count运算

COUNTSCAN

count不使用用Index进行count时的stage返回

COUNT_SCAN

count使用了Index进行count时的stage返回

SUBPLA

未使用到索引的$or查询的stage返回

TEXT

使用全文索引进行查询时候的stage返回

PROJECTION

限定返回字段时候stage的返回

部分源码如下:

mongo/jstests/libs/analyze_plan.js

/**
 * A query is covered iff it does *not* have a FETCH stage or a COLLSCAN.
 *
 * Given the root stage of explain's BSON representation of a query plan ('root'),
 * returns true if the plan is index only. Otherwise returns false.
 */
function isIndexOnly(root) {
    return !planHasStage(root, "FETCH") && !planHasStage(root, "COLLSCAN");
}

/**
 * Returns true if the BSON representation of a plan rooted at 'root' is using
 * an index scan, and false otherwise.
 */
function isIxscan(root) {
    return planHasStage(root, "IXSCAN");
}

/**
 * Returns true if the BSON representation of a plan rooted at 'root' is using
 * the idhack fast path, and false otherwise.
 */
function isIdhack(root) {
    return planHasStage(root, "IDHACK");
}

/**
 * Returns true if the BSON representation of a plan rooted at 'root' is using
 * a collection scan, and false otherwise.
 */
function isCollscan(root) {
    return planHasStage(root, "COLLSCAN");
}

/**
 * Get the number of chunk skips for the BSON exec stats tree rooted at 'root'.
 */
function getChunkSkips(root) {
    if (root.stage === "SHARDING_FILTER") {
        return root.chunkSkips;
    }
    else if ("inputStage" in root) {
        return getChunkSkips(root.inputStage);
    }
    else if ("inputStages" in root) {
        var skips = 0;
        for (var i = 0; i < root.inputStages.length; i++) {
            skips += getChunkSkips(root.inputStages[0]);
        }
        return skips;
    }

mongo/jstests/concurrency/fsm_workloads/explain_count.js

$config.states = Object.extend({
        explainBasicCount: function explainBasicCount(db, collName) {
            var res = db[collName].explain().count();
            assertAlways.commandWorked(res);
            assertAlways(planHasStage(res.queryPlanner.winningPlan, 'COUNT'));
        },
        explainCountHint: function explainCountHint(db, collName) {
            assertWhenOwnColl(function() {
                var res = db[collName].explain()
                                      .find({ i: this.nInserted / 2 })
                                      .hint({ i: 1 }).count();
                assertWhenOwnColl.commandWorked(res);
                assertWhenOwnColl(planHasStage(res.queryPlanner.winningPlan, 'COUNT'));
                assertWhenOwnColl(planHasStage(res.queryPlanner.winningPlan, 'COUNT_SCAN'));
            });

mongo/jstests/concurrency/fsm_workloads/explain_find.js

$config.states = Object.extend({
        explainLimit: function explainLimit(db, collName) {
            var res = db[collName].find().limit(3).explain();
            assertAlways.commandWorked(res);
            assertAlways(planHasStage(res.queryPlanner.winningPlan, 'LIMIT'));
        },
        explainBatchSize: function explainBatchSize(db, collName) {
            var res = db[collName].find().batchSize(3).explain();
            assertAlways.commandWorked(res);
        },
        explainAddOption: function explainAddOption(db, collName) {
            var res = db[collName].explain().find().addOption(DBQuery.Option.exhaust).finish();
            assertAlways.commandWorked(res);
        },
        explainSkip: function explainSkip(db, collName) {
            var res = db[collName].explain().find().skip(3).finish();
            assertAlways.commandWorked(res);
            assertAlways(planHasStage(res.queryPlanner.winningPlan, 'SKIP'));
        },
        explainSort: function explainSort(db, collName) {
            var res = db[collName].find().sort({ i: -1 }).explain();
            assertAlways.commandWorked(res);
            assertAlways(planHasStage(res.queryPlanner.winningPlan, 'SORT'));
        },
/**
 * The SubplanStage is used for rooted $or queries. It plans each clause of the $or
 * individually, and then creates an overall query plan based on the winning plan from
 * each clause.
 *
 * Uses the MultiPlanStage in order to rank plans for the individual clauses.
 *
 * Notes on caching strategy:
 *
 *   --Interaction with the plan cache is done on a per-clause basis. For a given clause C,
 *   if there is a plan in the cache for shape C, then C is planned using the index tags
 *   obtained from the plan cache entry. If no cached plan is found for C, then a MultiPlanStage
 *   is used to determine the best plan for the clause; unless there is a tie between multiple
 *   candidate plans, the winner is inserted into the plan cache and used to plan subsequent
 *   executions of C. These subsequent executions of shape C could be either as a clause in
 *   another rooted $or query, or shape C as its own query.
 *
 *   --Plans for entire rooted $or queries are neither written to nor read from the plan cache.
 */

allPlansExecution

顾名思义,allPlansExecution模式是将所有的执行计划均进行executionStats模式的操作,不在此赘述了。

后文

第二部分完。

以上便是explain的主要返回解析,那么,针对这么多返回项,我们应该关注什么呢?

在最后一部分中,我们将对该如何分析exlain信息进行详细解读,并将针对实例进行explain分析详解。

尽请期待。

关于作者

周李洋,社区常用ID eshujiushiwo,关注Mysql与MongoDB技术,数据架构,服务器架构等,现就职于DeNA,mongo-mopre,mongo-mload作者,任CSDN mongodb版主,MongoDB上海用户组发起人,MongoDB官方翻译组核心成员,MongoDB中文站博主,MongoDB Contribution Award获得者,MongoDB Days Beijing 2014演讲嘉宾。
联系方式:378013446
MongoDB上海用户组:192202324
欢迎交流。

发表评论