索引優化是一個永遠都繞不過的話題,做爲NoSQL的MongoDB也不例外。Mysql中經過explain命令來查看對應的索引信息,MongoDB亦如此。css
1. db.collection.explain().<method(...)>
db.products.explain().remove( { category: "apparel" }, { justOne: true })
2. db.collection.<method(...)>.explain({})
db.products.remove( { category: "apparel" }, { justOne: true }).explain()
複製代碼
若是你是在mongoshell 中第一種和第二種沒什麼區別,若是你是在robot 3T這樣的客戶端工具中使用你必須在後面顯示調用finish()或者next()web
db.collection.explain().find({}).finish()
複製代碼
explain有三種模式,分別是:sql
explain("allPlansExecution") = explain({})
日誌表中存儲了用戶的操做日誌,咱們常常查詢某一篇文章的操做日誌,數據以下:shell
{
"_id" : NumberLong(7277744),
"operatorName" : "autotest_cp",
"operateTimeUnix" : NumberLong(1586511800890),
"module" : "ARTICLE",
"opType" : "CREATE",
"level" : "GENERAL",
"recordData" : {
"articleId" : "6153324",
"categories" : "100006",
"title" : "testCase-2 this article is created for cp edior to search",
"status" : "DRAFT"
},
"responseCode" : 10002
}
複製代碼
集合中大概有700萬數據,對於這樣的查詢語句json
db.getCollection('operateLog').find({"module": "ARTICLE", "recordData.articleId": "6153324"}).sort({_id:-1})
複製代碼
首先看下queryPlanner返回的內容:bash
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "smcp.operateLog",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"module" : {
"$eq" : "ARTICLE"
}
},
{
"recordData.articleId" : {
"$eq" : "6153324"
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"module" : {
"$eq" : "ARTICLE"
}
},
{
"recordData.articleId" : {
"$eq" : "6153324"
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"_id" : 1
},
"indexName" : "_id_",
"isMultiKey" : false,
"multiKeyPaths" : {
"_id" : []
},
"isUnique" : true,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"_id" : [
"[MaxKey, MinKey]"
]
}
}
},
"rejectedPlans" : [
{
"stage" : "SORT",
"sortPattern" : {
"_id" : -1.0
},
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"recordData.articleId" : {
"$eq" : "6153324"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"module" : 1.0,
"opType" : 1.0
},
"indexName" : "module_1_opType_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"module" : [],
"opType" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"module" : [
"[\"ARTICLE\", \"ARTICLE\"]"
],
"opType" : [
"[MinKey, MaxKey]"
]
}
}
}
}
}
]
}
複製代碼
一些重要字段的含義app
queryPlanner.namespace
查詢的哪一個表工具
queryPlanner.winningPlan
查詢優化器針對該query所返回的最優執行計劃的詳細內容。post
queryPlanner.winningPlan.stage
最優計劃執行的階段,每一個階段都包含特定於該階段的信息。例如,IXSCAN階段將包括索引範圍以及特定於索引掃描的其餘數據。若是一個階段具備一個子階段或多個子階段,那麼該階段將具備inputStage或inputStages。性能
queryPlanner.winningPlan.inputStage
描述子階段的文檔,該子階段向其父級提供文檔或索引鍵。若是父階段只有一個孩子,則該字段存在。
queryPlanner.winningPlan.inputStage.indexName
winning plan所選用的index,這裏是根據_id來進行排序的,因此使用了_id的索引
queryPlanner.winningPlan.inputStage.isMultiKey
是不是Multikey,此處返回是false,若是索引創建在array上,此處將是true
queryPlanner.winningPlan.inputStage.isUnique
使用的索引是不是惟一索引,這裏的_id是惟一索引
queryPlanner.winningPlan.inputStage.isSparse
是不是稀疏索引
queryPlanner.winningPlan.inputStage.isPartial
是不是部分索引
queryPlanner.winningPlan.inputStage.direction
此query的查詢順序,默認是forward,因爲使用了sort({_id:-1})顯示backward
queryPlanner.winningPlan.inputStage.indexBounds
winningplan所掃描的索引範圍,因爲這裏使用的是sort({_id:-1}),對_id倒序排序,因此範圍是[MaxKey,MinKey]。若是是正序,則是[MinKey,MaxKey]
queryPlanner.rejectedPlans
拒絕的計劃詳細內容,各字段含義同winningPlan
再來看下executionStats的返回結果
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 24387,
"totalKeysExamined" : 6998084,
"totalDocsExamined" : 6998084,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"module" : {
"$eq" : "ARTICLE"
}
},
{
"recordData.articleId" : {
"$eq" : "6153324"
}
}
]
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 1684,
"works" : 6998085,
"advanced" : 1,
"needTime" : 6998083,
"needYield" : 0,
"saveState" : 71074,
"restoreState" : 71074,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 6998084,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 6998084,
"executionTimeMillisEstimate" : 290,
"works" : 6998085,
"advanced" : 6998084,
"needTime" : 0,
"needYield" : 0,
"saveState" : 71074,
"restoreState" : 71074,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"_id" : 1
},
"indexName" : "_id_",
"isMultiKey" : false,
"multiKeyPaths" : {
"_id" : []
},
"isUnique" : true,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"_id" : [
"[MaxKey, MinKey]"
]
},
"keysExamined" : 6998084,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
},
"allPlansExecution" : [
{...},
{...}
]
}
複製代碼
executionStats.executionSuccess
是否執行成功
executionStats.nReturned
查詢的返回條數
executionStats.executionTimeMillis
選擇查詢計劃和執行查詢所需的總時間(以毫秒爲單位)
executionStats.totalKeysExamined
索引掃描次數
executionStats.totalDocsExamined
document掃描次數
executionStats.executionStages
以階段樹的形式詳細說明獲勝計劃的完成執行狀況;即一個階段能夠具備一個inputStage或多個inputStages。如上說明。
executionStats.executionStages.inputStage.keysExamined
掃描了多少次索引
executionStats.executionStages.inputStage.docsExamined
掃描了多少次文檔,通常當stage是 COLLSCAN的時候會有這個值。
exlexecutionStats.allPlansExecution
這裏展現了全部查詢計劃的詳細。(winningPlan + rejectPlans),字段含義和winningPlan中一致,不作贅述
從上面能夠看出stage是很重要的,一個查詢到底走的是索引仍是全表掃描主要看的就是stage的值, 而stage有以下值
{
country: "ID",
name: "jjj",
status: 0
},
{
country: "ZH",
name: "lisi",
status: 1
}
複製代碼
咱們對country字段創建了索引,同時執行下面的語句
db.getCollection('testData').explain(true).count({country: "ID"})
複製代碼
那麼查看執行結果能夠看到 executionStats.executionStages.inputStage.stage = COUNT_SCAN, COUNT_SCAN是COUNT的一個子階段。
db.getCollection('testData').explain(true).count({status: 0})
複製代碼
此時 executionStats.executionStages.inputStage.stage = COUNTSCAN , COUNTSCAN是COUNT的一個子階段
db.getCollection('testData').find({$or : [{name : "lisi"}, {status: 0}]}).explain(true);
複製代碼
此時 executionStats.executionStages.stage = SUBPLAN
查看executionStats.executionStages.stage以及其下各個inputStage(子階段)的值是什麼,能夠斷定存在哪些優化點。
一個查詢它掃描的文檔數要儘量的少,才能更快,明顯咱們咱們不但願看到COLLSCAN, SORT_KEY_GENERATOR, COUNTSCAN, SUBPLAN 以及不合理的 SKIP 這些stage,當你看到這些stage的時候就要注意了。
當你看winningPlan或者rejectPlan的時候,你就能夠知道執行順序是怎樣的,好比咱們rejectPlan中,先是經過 "module_1_opType_1"檢索 "module = ARTICLE"的數據,而後FETCH階段再經過 "recordData.articleId=6153324"進行過濾,最後在內存中排序後返回數據。 明顯這樣的計劃被拒絕了,至少它沒有winningPlan執行快。
再來看看executionStats返回的數據
nReturned 爲 1,即符合條件的只有1條
executionTimeMillis 值爲24387,執行時間爲24秒
totalKeysExamined 值爲 6998084,雖然用到了索引,可是幾乎是掃描了全部的key
totalDocsExamined的值爲6998084,也是掃描了全部文檔
從上面的輸出結果能夠看出來,雖然咱們使用了索引,可是速度依然很慢。很明顯如今的索引,並不適合咱們,爲了排除干擾,咱們先將module_1_opType_1這個索引刪除。因爲咱們這裏使用了兩個字段進行查詢,而 recordData.articleId這個字段並非每一個document(集合中還存儲了其餘類型的數據)都存在,因此創建索引的時候recordData.articleId須要創建部分索引
db.getCollection('operateLog').createIndex(
{'module': 1, 'recordData.articleId': 1 },
{
"partialFilterExpression": {
"recordData.articleId": {
"$exists": true
}
},
"background": true
}
)
複製代碼
我先吃個蘋果,等它把索引創建好,你們有啥吃啥。在索引創建完成以後,咱們來看看 executionStats 的結果
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 3,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "SORT",
"sortPattern" : {
"_id" : -1.0
},
"memUsage" : 491,
"memLimit" : 33554432,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 1,
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"module" : 1.0,
"recordData.articleId" : 1.0
},
"indexName" : "module_1_recordData.articleId_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"module" : [],
"recordData.articleId" : []
},
"isPartial" : true,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"module" : [
"[\"ARTICLE\", \"ARTICLE\"]"
],
"recordData.articleId" : [
"[\"6153324\", \"6153324\"]"
]
}
}
}
}
}
}
複製代碼
我忽略了一些不重要的字段,能夠看到,如今執行時間是3毫秒(executionTimeMillis=3),掃描了1個index(totalKeysExamined=1),掃描了1個document(totalDocsExamined=1)。相比於以前的24387毫秒,我能夠說個人執行速度提高了8000倍,我就問還有誰。若是此事讓UC 震驚部小編知道了,確定又能夠起一個震驚的標題了
可是這個執行計劃仍然有問題,有問題,有問題,重要的事情說三遍。 executionStages.stage = sort,證實它在內存中排序了,在數據量大的時候,是很消耗性能的,因此千萬不能忽視它,咱們要改進這個點。
咱們要在 nReturned = totalDocsExamined的基礎上,讓排序也走索引。因此咱們先將以前的索引刪除,而後從新建立索引,這裏咱們將_id字段也加入到索引中,三個字段造成組合索引
db.getCollection('operateLog').createIndex(
{'module': 1, 'recordData.articleId': 1, '_id': -1},
{
"partialFilterExpression": {
"recordData.articleId": {
"$exists": true
}
},
"background": true
}
)
複製代碼
一樣的再來看看咱們的執行結果:
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"docsExamined" : 1,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 1,
"keyPattern" : {
"module" : 1.0,
"recordData.articleId" : 1.0,
"_id" : -1.0
},
"indexName" : "module_1_recordData.articleId_1__id_-1",
"multiKeyPaths" : {
"module" : [],
"recordData.articleId" : [],
"_id" : []
},
"isPartial" : true,
"direction" : "forward",
"indexBounds" : {
"module" : [
"[\"ARTICLE\", \"ARTICLE\"]"
],
"recordData.articleId" : [
"[\"6153324\", \"6153324\"]"
],
"_id" : [
"[MaxKey, MinKey]"
]
}
}
}
}
複製代碼
能夠看到咱們此次的stage是FETCH+IXSCAN,同時 nReturned = totalKeysExamined = totalDocsExamined = 1,而且利用了index排序,而非在內存中排序。從executionTimeMillis=0也能夠看出來,性能相比於以前的3毫秒也有所提高,至此這個索引就是咱們須要的了。
最開頭的結果和優化的過程告訴咱們,使用了索引你的查詢仍然可能很慢,咱們要將更多的目光集中到掃描的文檔或者行數中。
根據ESR原則建立索引
精確(Equal)匹配的字段放最前面,排序(Sort)條件放中間,範圍(Range)匹配的字段放最後面,一樣適用於ES,ER。
每個查詢都必需要有對應的索引
儘可能使用覆蓋索引 Covered Indexes(能夠避免讀數據文件)
須要查詢的條件以及返回值均在索引中
使用 projection 來減小返回到客戶端的的文檔的內容
儘量不要計算總數,特別是數據量大和查詢不能命中索引的時候
避免使用skip/limit形式的分頁,特別是數據量大的時候
替代方案:使用查詢條件+惟一排序條件
第一頁:db.posts.find({}).sort({_id: 1}).limit(20)
第二頁:db.posts.find({_id: {$gt: <第一頁最後一個_id>}}).sort({_id: 1}).limit(20)
第三頁:db.posts.find({_id: {$gt: <第二頁最後一個_id>}}).sort({_id: 1}).limit(20)