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"http://查詢方向
},
"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" : [ ]
},
...
}