二,explain与索引

2021-01-11  本文已影响0人  alexgu

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" : [ ]
        },
        ...
}
上一篇下一篇

猜你喜欢

热点阅读