一、MongoDB查詢優化器
1、MongoDB查詢優化器
1)MongoDB查詢優化器會選擇最優的一條執行計劃來執行SQL。
2)查詢優化器會緩存那些有多條可用索引的SQL的執行計劃條目
2、查詢優化器原理
1)對于每個SQL,查詢優化器會先在在執行計劃緩存中查找執行計劃
2)如果沒有比對到相應的執行計劃,查詢優化器會生成備選執行計劃,并評估他們各自的消耗,選擇中最佳執行計劃,并将23)這些執行計劃放入緩存中
3)MongoDB根據最優執行計劃傳回結果
4)如果比對到可用的執行計劃,MongoDB會通過replanning的機制再次評估目前執行計劃的性能
5)如果評估成功,使用該執行計劃傳回結果
6)如果評估失敗,查詢優化器重複2)操作,最終選擇最優執行計劃傳回結果
3、執行計劃緩存重新整理機制
1)删除集合或者索引會重新重新整理執行計劃緩存
2)執行計劃緩存在MongoDB重新開機後會失效
3)MongoDB2.6版本之後可以執行db.collection.getPlanCache().clear()手動重新整理執行計劃緩存
二、執行計劃解析
1、文法
方法一:
db.collection.find().explain()
檢視help幫助文檔:
db.collection.explain().help()
方法二:
db.collection.explain().find()
db.collection.explain().find().help()
2、執行計劃的三種模式
queryPlanner Mode:隻會顯示 winning plan 的 queryPlanner,自建MongoDB預設模式
executionStats Mode:隻會顯示 winning plan 的 queryPlanner + executionStats
allPlansExecution Mode:會顯示所有執行計劃的 queryPlanner + executionStats,阿裡雲MongoDB預設模式
不論哪種模式下,檢視一個SQL的執行計劃,都是通過查詢優化器來判斷的,對于所有寫操作,查詢執行計劃隻會限制起操作的消耗,不會執行操作進行變更。
3、Mongodb 執行計劃解析
MongoDB執行計劃主要分為兩個部分:queryPlanner、executionStats
示例集合:
db.asir.find().limit(1).pretty()
{
"_id" : ObjectId("5d3954a3cd19f9203957cea4"),
"id" : 0,
"name" : "sakw",
"age" : 18,
"date" : ISODate("2019-07-25T07:05:07.695Z")
}
db.asir.createIndex({age:1})
db.asir.createIndex({name:1,age:1})
示例查詢:
db.asir.find({name:"aa",age:{$gte:28},date:ISODate("2019-07-25T07:05:07.695Z")}).explain("executionStats")
1、queryPlanner
queryPlanner主要有三大部分:parsedQuery、winningPlan、rejectedPlans。
我們關注比較多的是winningPlan,檢視SQL目前執行走了什麼索引
queryPlanner: {
plannerVersion: 1,
namespace: "test.asir", //database.collection
indexFilterSet: false, //針對該query是否有indexfilter
parsedQuery: { //執行計劃解析
},
winningPlan: { //最終執行計劃
},
rejectedPlans: [ //競争執行計劃
]
}
1)parsedQuery - SQL解析
該部分解析了SQL的所有過濾條件
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.asir", //database.collection
"indexFilterSet" : false, //該類查詢是否使用indexfilter
"parsedQuery" : { //查詢解析
"$and" : [
{
"date" : {
"$eq" : ISODate("2019-07-25T07:05:07.695Z")
}
},
{
"name" : {
"$eq" : "aa"
}
},
{
"age" : {
"$gte" : 28
}
}
]
},
2)winningPlan - SQL最終選擇的執行計劃
winningPlan可以分三部分來看:stage、filter、inputStage
"winningPlan" : { //最終選擇執行計劃
"stage" : "FETCH", //通過索引檢索得到記錄再次進行文檔檢索
"filter" : {
"date" : { //文檔檢索條件
"$eq" : ISODate("2019-07-25T07:05:07.695Z")
}
},
"inputStage" : { //子stage
"stage" : "IXSCAN", //索引檢索
"keyPattern" : { //索引檢索條件
"name" : 1,
"age" : 1
},
"indexName" : "name_1_age_1", //索引名字
"isMultiKey" : false, //是否為多鍵索引
"multiKeyPaths" : {
"name" : [ ],
"age" : [ ]
},
"isUnique" : false, //是否為唯一索引
"isSparse" : false, //是否為稀疏索引
"isPartial" : false, //是否為部分索引
"indexVersion" : 2, //索引版本
"direction" : "forward",
"indexBounds" : { //索引範圍
"name" : [
"[\"aa\", \"aa\"]"
],
"age" : [
"[28.0, inf.0]"
]
}
}
},
3)rejectedPlans - 被淘汰的執行計劃
"rejectedPlans" : [] //競選失敗的執行計劃
2、executionStats
最好的情況是:nReturned = totalKeysExamined = totalDocsExamined
"executionStats" : {
"executionSuccess" : true, //是否執行成功
"nReturned" : 1, //傳回記錄數
"executionTimeMillis" : 0, //SQL總執行時間消耗,ms
"totalKeysExamined" : 11, //索引掃描數
"totalDocsExamined" : 11, //文檔掃描數
"executionStages" : {
"stage" : "FETCH", //通過索引檢索得到記錄再次進行文檔掃描的過程
"filter" : {
"date" : {
"$eq" : ISODate("2019-07-25T07:05:07.695Z")
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 0, //文檔掃描時間消耗,ms
"works" : 13, //期間所操作的工作單元個數
"advanced" : 1, //優先傳回給父stage的中間結果集中文檔個數
"needTime" : 10,
"needYield" : 0, //請求查詢階段暫停處理并産生鎖定的次數
"saveState" : 0, //查詢階段暫停處理并儲存其目前執行狀态的次數
"restoreState" : 0, //查詢階段恢複已儲存的執行狀态的次數
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 11,
"alreadyHasObj" : 0,
"inputStage" : { //索引檢索階段
"stage" : "IXSCAN",
"nReturned" : 11,
"executionTimeMillisEstimate" : 0,
"works" : 12,
"advanced" : 11,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"name" : 1,
"age" : 1
},
"indexName" : "name_1_age_1", //使用索引名稱
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ],
"age" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"aa\", \"aa\"]"
],
"age" : [
"[28.0, inf.0]"
]
},
"keysExamined" : 11,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
},
3、serverInfo 伺服器資訊
阿裡雲MonogoDB執行個體上其實将這個資訊隐藏掉了。
"serverInfo" : {
"host" : "dbslave2", //主機
"port" : 28002, //端口
"version" : "4.0.10-5", //版本
"gitVersion" : "7dab0a3a7b7b40cf71724b5a11eff871f8c3885c" //MongoDB Server git版本号
},
4、indexFilterSet
indexFilter僅僅決定對于該查詢MongoDB可選擇的索引是由什麼決定的。若indexFilterSet為true,說明該查詢隻能選擇indexFilter設定的一些可選索引,最終選擇使用哪個索引由優化器決定;若indexFilterSet=false,說明該查詢可以選擇該集合所有的索引,最終選擇使用哪個索引由優化器确定。
1)如何設定indexFilter
db.runCommand(
{
planCacheSetFilter: <collection>, //需要建立indexFilter集合
query: <query>, //指定哪類查詢使用indexFilter
sort: <sort>, //排序條件
projection: <projection>, //查詢字段
indexes: [ <index1>, <index2>, ...] //indexFilter可使用索引
}
)
2)如何删除indexFilter
planCacheClearFilters: <collection>, //指定集合
query: <query pattern>, //指定查詢類别
sort: <sort specification>, //排序條件
projection: <projection specification> //查詢字段
3)如何檢視一個集合所有的indexFilter
db.runCommand( { planCacheListFilters: <collection> } )
4)示例:
集合資料如下:
db.scores.find()
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("5d303213cd8afaa592e23990"), "userid" : "AAAAAAA", "score" : 43 }
{ "_id" : ObjectId("5d303213cd8afaa592e23991"), "userid" : "BBBBBBB", "score" : 34 }
{ "_id" : ObjectId("5d303213cd8afaa592e23992"), "userid" : "CCCCCCC" }
{ "_id" : ObjectId("5d303213cd8afaa592e23993"), "userid" : "DDDDDDD" }
db.scores.createIndex({userid:1,score:1})
建立indexFilter:
> db.runCommand(
... {
... planCacheSetFilter: "scores",
... query: { userid: "abby" },
... indexes: [
... { "userid" : 1, "score" : 1},
... ]
... }
... )
{ "ok" : 1 }
檢視執行計劃:
> db.scores.find({userid: "abbyc"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.scores",
"indexFilterSet" : true, //表示使用了indexFilter指定的索引
"parsedQuery" : {
"userid" : {
"$eq" : "abbyc"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"userid" : 1,
"score" : 1
},
"indexName" : "userid_1_score_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"userid" : [ ],
"score" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"userid" : [
"[\"abbyc\", \"abbyc\"]"
],
"score" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "dbslave2",
"port" : 28002,
"version" : "4.0.10-5",
"gitVersion" : "7dab0a3a7b7b40cf71724b5a11eff871f8c3885c"
},
"ok" : 1
}
5)注意點
當使用index filter的時候,使用hint強制走index filter之外的索引會失效,
4、stage類型
stage的類型:
COLLSCAN:全表掃描
IXSCAN:索引掃描
FETCH:根據索引去檢索指定document
SHARD_MERGE:将各個分片傳回資料進行merge
SORT:表明在記憶體中進行了排序
LIMIT:使用limit限制傳回數
SKIP:使用skip進行跳過
IDHACK:針對_id進行查詢
SHARDING_FILTER:通過mongos對分片資料進行查詢
COUNT:利用db.coll.explain().count()之類進行count運算
COUNTSCAN:count不使用Index進行count時的stage傳回
COUNT_SCAN:count使用了Index進行count時的stage傳回
SUBPLA:未使用到索引的$or查詢的stage傳回
TEXT:使用全文索引進行查詢時候的stage傳回
PROJECTION:限定傳回字段時候stage的傳回
對于普通查詢,我希望看到stage的組合(查詢的時候盡可能用上索引):
Fetch+IDHACK
Fetch+ixscan
Limit+(Fetch+ixscan)
PROJECTION+ixscan
SHARDING_FITER+ixscan
COUNT_SCAN
如下的stage效率比較低下:
COLLSCAN(全表掃描)
SORT(使用sort但是無index)
SUBPLA(未用到index的$or)
COUNTSCAN(不使用index進行count)
三、如何排查MongoDB性能問題
1、對于目前正在發生的情況
1)檢視目前會話情況,抓取正在慢的SQL
db.currentOp()
或者
db.currentOp(
{
"active" : true,
"secs_running" : { "$gt" : 3 },
"ns" : /^db1\./
}
)
重點關注:
client #請求是由哪個用戶端發起
opid #操作的opid,可以通過 db.killOp(opid) 直接殺掉會話
secs_running/microsecs_running
#這個值重點關注,代表請求運作的時間,如果這個值特别大,就得注意了,看看請求是否合理
query/ns: #這個能看出是對哪個集合正在執行什麼操作
lock*: #還有一些跟鎖相關的參數
2)檢視問題SQL執行計劃
db.collection.find().explain()
2、對于曆史問題
1)檢視慢日志以及運作日志
如何調整慢日志參數:
檢視目前慢日志設定參數
> db.getProfilingStatus() //檢視目前慢日志參數狀态
{ "was" : 1, "slowms" : 100 }
動态修改慢日志參數
> db.setProfilingLevel(1,10) //動态修改參數
{ "was" : 1, "slowms" : 100, "ok" : 1 }
> db.getProfilingStatus()
{ "was" : 1, "slowms" : 10 }
慢日志參數解釋:
was: 慢日志模式
0:不開啟慢日志
1:開啟慢日志,隻記錄超過一定門檻值的慢SQL
2:開啟慢日志,記錄所有操作
slowms:慢SQL門檻值,機關為ms
檢視慢日志資訊:
> db.system.profile.find().sort({$natrual: -1}) //查詢最近慢日志記錄
db.collection.find().explain()
四、讀寫操作的一些優化
1、查詢優化
1)建立合适索引
1.在選擇性較好的字段建立索引
2.單列索引不需要考慮升降序,但是複合索引可以根據業務需求建立對應升降序的複合索引
3.覆寫索引查詢,查詢和過濾的所有字段都在複合索引中
db.inventory.createIndex( { type: 1, item: 1 } )
db.inventory.find(
{ type: "food", item:/^c/ },
{ item: 1, _id: 0 } ) //需要強制_id字段不顯示以保證走覆寫索引查詢
2)使用limit限定傳回結果,減輕網絡開銷
3)需要哪些字段查詢哪些字段
4)使用hint強制走指定索引
2、寫操作優化
1)集合上的索引會增加該集合寫入/更新操作的資源消耗,适度建立索引
MMAPv1存儲引擎中,當一個update操作所需要的空間超過了原本配置設定的空間時,MMAPv1存儲引會将該文檔移動到磁盤上一個新的位置,并全部将該集合的索引進行更新指向新的文檔位置,整個過程是非常消耗資源的。
從MongoDB 3.0開始,MonogoDB使用 Power of 2 Sized Allocations,保證MongoDB盡量對空間的空間重用,盡量減少重新配置設定空間位置的發生。
2)硬體優化,固态SSD的性能要優于HDDs
3)合理設定journal相關參數
1.journal日志實作日志預寫功能,開啟journal保證了資料的持久化,但也存在一定的性能消耗
2.盡量将資料檔案與journal日志檔案放在不同的磁盤喜愛,避免I/O資源争用,提高寫操作能力
3.j:true參數會增加寫操作的負載,根據業務實際情況合理使用write concern參數
4.設定合理的commitIntervalMs參數
減小該參數會減少日志送出的時間間隔、增加寫操作的數量,但是會限制MongoDB寫能力。
增大該參數會增加日志送出的時間間隔、減少寫操作的數量,但是增加了MongoDB意外當機期間日志沒有落盤的可能。