二,explain与索引
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" : [ ]
},
...
}