查看執(zhí)行計劃
索引優(yōu)化是一個永遠都繞不過的話題,作為NoSQL的MongoDB也不例外死嗦。Mysql中通過explain命令來查看對應的索引信息,MongoDB亦如此肪笋。
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 中第一種和第二種沒什么區(qū)別窒悔,如果你是在robot 3T這樣的客戶端工具中使用你必須在后面顯示調(diào)用finish()或者next()
db.collection.explain().find({}).finish()
explain有三種模式,分別是:
- queryPlanner(默認) :queryPlanner模式下并不會去真正進行query語句查詢呜袁,而是針對query語句進行執(zhí)行計劃分析并選出winning plan
- executionStats :MongoDB運行查詢優(yōu)化器以選擇獲勝計劃(winning plan),執(zhí)行獲勝計劃直至完成简珠,并返回描述獲勝計劃執(zhí)行情況的統(tǒng)計信息阶界。
- allPlansExecution: queryPlanner和executionStats都返回。相當于
explain("allPlansExecution") = explain({})
queryPlanner(查詢計劃)
日志表中存儲了用戶的操作日志,我們經(jīng)常查詢某一篇文章的操作日志,數(shù)據(jù)如下:
{
"_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萬數(shù)據(jù),對于這樣的查詢語句
db.getCollection('operateLog').find({"module": "ARTICLE", "recordData.articleId": "6153324"}).sort({_id:-1})
首先看下queryPlanner返回的內(nèi)容:
"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]"
]
}
}
}
}
}
]
}
字段含義
一些重要字段的含義
queryPlanner.namespace
查詢的哪個表queryPlanner.winningPlan
查詢優(yōu)化器針對該query所返回的最優(yōu)執(zhí)行計劃的詳細內(nèi)容聋庵。queryPlanner.winningPlan.stage
最優(yōu)計劃執(zhí)行的階段,每個階段都包含特定于該階段的信息膘融。例如,IXSCAN階段將包括索引范圍以及特定于索??引掃描的其他數(shù)據(jù)祭玉。如果一個階段具有一個子階段或多個子階段氧映,那么該階段將具有inputStage或inputStages。queryPlanner.winningPlan.inputStage
描述子階段的文檔攘宙,該子階段向其父級提供文檔或索引鍵屯耸。如果父階段只有一個孩子,則該字段存在蹭劈。queryPlanner.winningPlan.inputStage.indexName
winning plan所選用的index,這里是根據(jù)_id來進行排序的,所以使用了_id的索引queryPlanner.winningPlan.inputStage.isMultiKey
是否是Multikey线召,此處返回是false铺韧,如果索引建立在array上,此處將是truequeryPlanner.winningPlan.inputStage.isUnique
使用的索引是否是唯一索引缓淹,這里的_id是唯一索引queryPlanner.winningPlan.inputStage.isSparse
是否是稀疏索引queryPlanner.winningPlan.inputStage.isPartial
是否是部分索引queryPlanner.winningPlan.inputStage.direction
此query的查詢順序哈打,默認是forward,由于使用了sort({_id:-1})顯示backwardqueryPlanner.winningPlan.inputStage.indexBounds
winningplan所掃描的索引范圍,由于這里使用的是sort({_id:-1}),對_id倒序排序,所以范圍是[MaxKey,MinKey]讯壶。如果是正序,則是[MinKey,MaxKey]queryPlanner.rejectedPlans
拒絕的計劃詳細內(nèi)容,各字段含義同winningPlan
executionStats(執(zhí)行結(jié)果)
再來看下executionStats的返回結(jié)果
"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
是否執(zhí)行成功executionStats.nReturned
查詢的返回條數(shù)executionStats.executionTimeMillis
選擇查詢計劃和執(zhí)行查詢所需的總時間(以毫秒為單位)executionStats.totalKeysExamined
索引掃描次數(shù)executionStats.totalDocsExamined
document掃描次數(shù)executionStats.executionStages
以階段樹的形式詳細說明獲勝計劃的完成執(zhí)行情況料仗;即一個階段可以具有一個inputStage或多個inputStages。如上說明伏蚊。executionStats.executionStages.inputStage.keysExamined
掃描了多少次索引executionStats.executionStages.inputStage.docsExamined
掃描了多少次文檔立轧,一般當stage是 COLLSCAN的時候會有這個值。exlexecutionStats.allPlansExecution
這里展示了所有查詢計劃的詳細躏吊。(winningPlan + rejectPlans),字段含義和winningPlan中一致氛改,不做贅述
15種stage
從上面可以看出stage是很重要的,一個查詢到底走的是索引還是全表掃描主要看的就是stage的值, 而stage有如下值
- COLLSCAN : 掃描整個集合
- IXSCAN : 索引掃描(index scan)
- FETCH : 根據(jù)索引返回的結(jié)果去檢索文檔(如上我們的例子)
- SHARD_MERGE : 將各個分片返回數(shù)據(jù)進行merge
- SORT : 調(diào)用了sort方法,當出現(xiàn)這個階段的時候你可以看到memUsage以及memLimit這兩個字段
- SORT_KEY_GENERATOR : 在內(nèi)存中進行了排序
- LIMIT : 使用limit限制返回數(shù)
- SKIP : 使用skip進行跳過
- IDHACK : 針對_id進行查詢
- SHARDING_FILTER :通過mongos對分片數(shù)據(jù)進行查詢
- COUNT: 利用db.coll.explain().count()之類進行count運算, 只要調(diào)用了count方法比伏,那么 executionStats.executionStages.stage = COUNT
- COUNT_SCAN : count使用Index進行count時的stage返回
{
country: "ID",
name: "jjj",
status: 0
},
{
country: "ZH",
name: "lisi",
status: 1
}
我們對country字段建立了索引胜卤,同時執(zhí)行下面的語句
db.getCollection('testData').explain(true).count({country: "ID"})
那么查看執(zhí)行結(jié)果可以看到 executionStats.executionStages.inputStage.stage = COUNT_SCAN, COUNT_SCAN是COUNT的一個子階段。
- COUNTSCAN : count不使用Index進行count時的stage返回赁项。
db.getCollection('testData').explain(true).count({status: 0})
此時 executionStats.executionStages.inputStage.stage = COUNTSCAN , COUNTSCAN是COUNT的一個子階段
- SUBPLAN : 未使用到索引的$or查詢的stage返回
db.getCollection('testData').find({$or : [{name : "lisi"}, {status: 0}]}).explain(true);
此時 executionStats.executionStages.stage = SUBPLAN
- TEXT : 使用全文索引進行查詢時候的stage返回
- PROJECTION : 限定返回字段時候stage的返回
查看executionStats.executionStages.stage以及其下各個inputStage(子階段)的值是什么,可以判定存在哪些優(yōu)化點葛躏。
一個查詢它掃描的文檔數(shù)要盡可能的少,才能更快澈段,明顯我們我們不希望看到COLLSCAN, SORT_KEY_GENERATOR, COUNTSCAN, SUBPLAN 以及不合理的 SKIP 這些stage,當你看到這些stage的時候就要注意了。
查詢優(yōu)化
當你看winningPlan或者rejectPlan的時候舰攒,你就可以知道執(zhí)行順序是怎樣的败富,比如我們rejectPlan中,先是通過 "module_1_opType_1"檢索 "module = ARTICLE"的數(shù)據(jù)芒率,然后FETCH階段再通過 "recordData.articleId=6153324"進行過濾囤耳,最后在內(nèi)存中排序后返回數(shù)據(jù)。 明顯這樣的計劃被拒絕了偶芍,至少它沒有winningPlan執(zhí)行快充择。
再來看看executionStats返回的數(shù)據(jù)
**
nReturned 為 1,即符合條件的只有1條
executionTimeMillis 值為24387,執(zhí)行時間為24秒
totalKeysExamined 值為 6998084,雖然用到了索引匪蟀,但是幾乎是掃描了所有的key
totalDocsExamined的值為6998084,也是掃描了所有文檔
**
從上面的輸出結(jié)果可以看出來椎麦,雖然我們使用了索引,但是速度依然很慢材彪。很明顯現(xiàn)在的索引观挎,并不適合我們,為了排除干擾段化,我們先將module_1_opType_1這個索引刪除嘁捷。由于我們這里使用了兩個字段進行查詢,而 recordData.articleId這個字段并不是每個document(集合中還存儲了其他類型的數(shù)據(jù))都存在显熏,所以建立索引的時候recordData.articleId需要建立部分索引
db.getCollection('operateLog').createIndex(
{'module': 1, 'recordData.articleId': 1 },
{
"partialFilterExpression": {
"recordData.articleId": {
"$exists": true
}
},
"background": true
}
)
我先吃個蘋果雄嚣,等它把索引建立好,大家有啥吃啥喘蟆。在索引建立完成之后缓升,我們來看看 executionStats 的結(jié)果
"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\"]"
]
}
}
}
}
}
}
我忽略了一些不重要的字段,可以看到,現(xiàn)在執(zhí)行時間是3毫秒(executionTimeMillis=3),掃描了1個index(totalKeysExamined=1),掃描了1個document(totalDocsExamined=1)蕴轨。相比于之前的24387毫秒港谊,我可以說我的執(zhí)行速度提升了8000倍,我就問還有誰橙弱。如果此事讓UC 震驚部小編知道了歧寺,肯定又可以起一個震驚的標題了
但是這個執(zhí)行計劃仍然有問題,有問題膘螟,有問題成福,重要的事情說三遍。 executionStages.stage = sort,證明它在內(nèi)存中排序了,在數(shù)據(jù)量大的時候荆残,是很消耗性能的奴艾,所以千萬不能忽視它,我們要改進這個點内斯。
我們要在 nReturned = totalDocsExamined的基礎上蕴潦,讓排序也走索引像啼。所以我們先將之前的索引刪除,然后重新創(chuàng)建索引潭苞,這里我們將_id字段也加入到索引中忽冻,三個字段形成組合索引
db.getCollection('operateLog').createIndex(
{'module': 1, 'recordData.articleId': 1, '_id': -1},
{
"partialFilterExpression": {
"recordData.articleId": {
"$exists": true
}
},
"background": true
}
)
同樣的再來看看我們的執(zhí)行結(jié)果:
"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排序此疹,而非在內(nèi)存中排序僧诚。從executionTimeMillis=0也可以看出來,性能相比于之前的3毫秒也有所提升蝗碎,至此這個索引就是我們需要的了湖笨。
最開頭的結(jié)果和優(yōu)化的過程告訴我們,使用了索引你的查詢?nèi)匀豢赡芎苈?我們要將更多的目光集中到掃描的文檔或者行數(shù)中。
索引優(yōu)化準則
根據(jù)ESR原則創(chuàng)建索引
精確(Equal)匹配的字段放最前面,排序(Sort)條件放中間,范圍(Range)匹配的字段放最后面,同樣適用于ES,ER蹦骑。每一個查詢都必須要有對應的索引
盡量使用覆蓋索引 Covered Indexes(可以避免讀數(shù)據(jù)文件)
需要查詢的條件以及返回值均在索引中使用 projection 來減少返回到客戶端的的文檔的內(nèi)容
盡可能不要計算總數(shù),特別是數(shù)據(jù)量大和查詢不能命中索引的時候
避免使用skip/limit形式的分頁慈省,特別是數(shù)據(jù)量大的時候
替代方案:使用查詢條件+唯一排序條件
第一頁:db.posts.find({}).sort({_id: 1}).limit(20);
第二頁:db.posts.find({_id: {gt: <第二頁最后一個_id>}}).sort({_id: 1}).limit(20);