二,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"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" : [ ]
        },
        ...
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容