美文网首页
二,explain与索引

二,explain与索引

作者: alexgu | 来源:发表于2021-01-11 02:42 被阅读0次

    2.1 查询的简单分析

    首先,先往数据表中插入10w条数据

    for (i=0;i<100000;i++){
      db.product.insertOne(
      {
        "id":"product-id-"+i,
        "name":"product-name-"+i,
        "price":123,
        "detail":"<html><body>hello world</body></html>",
        "sku":[
            {
                "id":"product-id-"+i+"-sku-"+i,
                "inventory":123
            }
        ],
        "createAt":new Date(),
        "updateAt":new Date(),
        "tag":[
            "red",
            "black"
        ]
      })
    }
    

    简单介绍下db.collection.explain(<verbose>).find(),代表对该查询语句进行分析。explain的参数为枚举值,分别代表explain的三种模式:

    模式 描述
    queryPlanner 执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等
    exectionStats 最佳执行计划的执行情况和被拒绝的计划等信息
    allPlansExecution 选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况

    执行

    db.product.explain("executionStats").find({ "name": "product-name-10000" })
    //返回
    {
            "queryPlanner" : {
                    "plannerVersion" : 1,
                    "namespace" : "alex.product",//db.collection格式,代表查询的db name与collection name
                    "indexFilterSet" : false,
                    "parsedQuery" : {//查询条件
                            "name" : {
                                    "$eq" : "product-name-10000"
                            }
                    },
                    "winningPlan" : {//mongo通过计划比对,得到的最佳查询计划
                            "stage" : "COLLSCAN",//查询方式,这种方式代表结合扫描
                            "filter" : {//过滤条件
                                    "name" : {
                                            "$eq" : "product-name-10000"
                                    }
                            },
                            "direction" : "forward"//查询方向
                    },
                    "rejectedPlans" : [ ]//拒绝计划
            },
            "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 1,//返回文档数
                    "executionTimeMillis" : 30,//语句执行时间
                    "totalKeysExamined" : 0,//索引扫描次数
                    "totalDocsExamined" : 100000,//文档扫描次数
                    "executionStages" : {
                            "stage" : "COLLSCAN",
                            "filter" : {
                                    "name" : {
                                            "$eq" : "product-name-10000"
                                    }
                            },
                            "nReturned" : 1,
                            "executionTimeMillisEstimate" : 3,//查询执行的估计时间(以毫秒为单位)
                            "works" : 100002,
                            "advanced" : 1,
                            "needTime" : 100000,
                            "needYield" : 0,//请求查询阶段暂停处理并产生其锁的次数
                            "saveState" : 100,
                            "restoreState" : 100,
                            "isEOF" : 1,
                            "direction" : "forward",
                            "docsExamined" : 100000
                    }
            },
            "serverInfo" : {
                    "host" : "iZ7xvd5tarkby8qjv4c4ynZ",
                    "port" : 27017,
                    "version" : "4.4.3",
                    "gitVersion" : "913d6b62acfbb344dde1b116f4161360acd8fd13"
            },
      
    

    从计划中的totalDocsExamined可知,每次查询都要把整个数据遍历一遍然后返回,相当于需要查询10w条记录。下面看下见索引后的情况
    首先,通过db.collection.createIndex()建立索引:

    /*
    表示对字段name按升序建立索引,如果为-1代表降序方式建立索引,
    生产环境记得配置background为true,配置方式可以参考官方文档
    */
    db.product.createIndex({"name":1})
    

    通过db.collection.getIndexes()查看当前索引:

    db.product.getIndexes()
    //返回
    [
            {
                    "v" : 2,
                    "key" : {
                            "_id" : 1
                    },
                    "name" : "_id_"
            },
            {
                    "v" : 2,
                    "key" : {
                            "name" : 1
                    },
                    "name" : "name_1"
            }
    ]
    

    可见,我们对name字段建立了索引,然后再执行下分析语句:

    db.product.explain("executionStats").find({ "name": "product-name-10000" })
    {
            "queryPlanner" : {
                    ...
                    "indexFilterSet" : false,
                    ...
                    "winningPlan" : {
                            "stage" : "FETCH",//这里通过子查询的之后需要进行回表
                            "inputStage" : {
                                    "stage" : "IXSCAN",//索引查询
                                    "keyPattern" : {
                                            "name" : 1
                                    },
                                    "indexName" : "name_1",//使用的索引名称
                                   ...
                            }
                    },
                    "rejectedPlans" : [ ]
            },
            "executionStats" : {
                    ...
                    "executionTimeMillis" : 0,
                    "totalKeysExamined" : 1,
                    "totalDocsExamined" : 1,
                    ...
                  }
            ...
    }
    

    建立索引查询之后totalKeysExamined索引查询的数量由0->1,totalDocsExamined由100000->1,这里之所以还有一次文档查询,是因为回表操作

    2.2 索引覆盖

    所谓的索引覆盖是指索引上面已包含需要返回的所有字段,无需再回表查询整个数据字段,例如上面的索引只返回name字段,就是索引覆盖

    //查询name大于"product-name-0",共有99999条数据,需要回表99999次
    db.product.explain("executionStats").find({ "name": {"$gt":"product-name-0"} })
    {
            "queryPlanner" : {
                    ...
                    "winningPlan" : {
                            "stage" : "FETCH",
                            "inputStage" : {
                                    "stage" : "IXSCAN",
                                    ...
                            }
                    },
            },
            "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 99999,
                    "executionTimeMillis" : 89,
                    "totalKeysExamined" : 99999,
                    "totalDocsExamined" : 99999,
                    ...
            },
           ...
    }
    
    
    //通过projection只返回name字段
    db.product.explain("executionStats").find({ "name": {"$gt":"product-name-0"} },{"name":1,"_id":0})
    {
            "queryPlanner" : {
                    ...
                    "winningPlan" : {
                            "stage" : "PROJECTION_COVERED",
                            "transformBy" : {
                                    "name" : 1,
                                    "_id" : 0
                            },
                            "inputStage" : {
                                    "stage" : "IXSCAN",
                                    ...
                            }
                    },
            },
            "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 99999,
                    "executionTimeMillis" : 44,
                    "totalKeysExamined" : 99999,
                    "totalDocsExamined" : 0,
                    ...
            },
            ...
    }
    

    对比两次查询情况,winningPlan.stage变成了PROJECTION_COVERED,totalDocsExamined变成了0,executionTimeMillis减少了一半。

    2.3 复合索引

    mongo可以由数据的多个字段建立一个索引,这种复合索引建立方式最好满足ESR原则,精确(Equal)匹配的字段放最前面,排序(Sort)条件放中间,范围(Range)匹配的字段放最后面。

    例如,上面的查询在没有复合索引的情况下根据价格排序:

    db.product.explain("executionStats").find({ "name": {"$gt":"product-name-0"} }).sort({"price":1})
    {
            "queryPlanner" : {
                    ...
                    "winningPlan" : {
                            "stage" : "SORT",
                            "sortPattern" : {
                                    "price" : 1
                            },
                            "memLimit" : 104857600,
                            "type" : "simple",
                            "inputStage" : {
                                    "stage" : "FETCH",
                                    "inputStage" : {
                                            "stage" : "IXSCAN",
                                            ...
                                    }
                            }
                    },
                    "rejectedPlans" : [ ]
            },
            "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 99999,
                    "executionTimeMillis" : 144,
                    "totalKeysExamined" : 99999,
                    "totalDocsExamined" : 99999,
                   ...
            },
    }
    

    可以看见winningPlan又多了一层,实际上查询过程:索引查询->回表->内存排序,下面建立复合索引,然后再分析一次查询:

    db.product.createIndex({"price":1,"name":1})
    
    db.product.explain("executionStats").find({ "name": {"$gt":"product-name-0"} }).sort({"price":1})
    {
            "queryPlanner" : {
                    ...
                    "winningPlan" : {
                            "stage" : "FETCH",
                            ...
                            "inputStage" : {
                                    "stage" : "IXSCAN",
                                    ...
                                    "indexName" : "price_1_name_1",
                                    ...
                            }
                    },
                    "rejectedPlans" : [
                            {
                                    "stage" : "SORT",
                                    ...
                            }
                    ]
            },
            "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 99999,
                    "executionTimeMillis" : 117,
                    "totalKeysExamined" : 100000,
                    "totalDocsExamined" : 100000,
                    ...
            },
           ...
    }
    

    对比原来的查询,winnerPlaner只有2层,rejectedPlans中显示的是原来的查询计划,executionTimeMillis少了30毫秒,另外创建索引db.product.createIndex({"price":1,"name":1}),而不是name在前,price在后的方式,是因为需要满足ESR原则,实际上是SR,name是一个范围过滤,如果创建索引时name放在前面,就无法利用索引排序,例如下面:

    db.product.createIndex({"name":1,"price":1})
    
    //强制使用"name_1_price_1"索引
    db.product.explain("executionStats").
    find({ "name": {"$gt":"product-name-0"} }).
    sort({"price":1}).
    hint("name_1_price_1")
    
    //返回
    {
            "queryPlanner" : {
                    "winningPlan" : {
                            "stage" : "FETCH",
                            "inputStage" : {
                                    "stage" : "SORT",
                                    "sortPattern" : {
                                            "price" : 1
                                    },
                                    "memLimit" : 104857600,
                                    "type" : "default",
                                    "inputStage" : {
                                            "stage" : "IXSCAN",
                                            ...
                                    }
                            }
                    },
                    "rejectedPlans" : [ ]
            },
            ...
    }
    

    相关文章

      网友评论

          本文标题:二,explain与索引

          本文链接:https://www.haomeiwen.com/subject/dsnlaktx.html