小李是這家公司的后端負(fù)責(zé)人,突然有一天下午帮掉,收到大量客服反饋用戶無法使用我們的APP,很多操作與加載都是網(wǎng)絡(luò)等待超時窒典。
收到信息后蟆炊,小李立馬排查問題原因,不過多一會瀑志,定位到數(shù)據(jù)庫出現(xiàn)大量慢查詢導(dǎo)致服務(wù)器超負(fù)荷負(fù)載狀態(tài)涩搓,CPU居高不下,那么為什么會出現(xiàn)這個情況呢劈猪,此時小李很慌昧甘,經(jīng)過查詢資料,開始往慢查詢方向探究战得,果不其然充边,由于業(yè)務(wù)數(shù)據(jù)增長迅猛,對應(yīng)的數(shù)據(jù)表沒有相應(yīng)查詢的索引數(shù)據(jù)常侦,此刻小李嘴角上揚浇冰,面露微笑,信心百倍上手的給數(shù)據(jù)庫相關(guān)數(shù)據(jù)表加上了索引字段聋亡。但是情況并沒有好轉(zhuǎn)肘习,線上依舊沒有恢復(fù),經(jīng)驗使然坡倔,最后只能采取降級的方案(關(guān)閉此表的相關(guān)查詢業(yè)務(wù))臨時先恢復(fù)線上正常井厌。
但是事情并沒有結(jié)束,問題沒有根本性的解決致讥,公司和自己依舊非常在意這個問題的解決,晚上吃飯的時候器赞,小李突然想起了自己有認(rèn)識一個行業(yè)大佬(老白)垢袱。把問題跟老白說了一遍,老白并沒過多久港柜,很快就專業(yè)的告訴了小白哪些操作存在問題请契,怎么樣可以正確的解決這個問題,加索引的時候首先要學(xué)會做查詢分析夏醉,然后了解ESR最佳實踐規(guī)則(下面會做說明)爽锥,小李沒有因為自己的不足感到失落,反而是因為自己的不足更是充滿了求知欲畔柔。
數(shù)據(jù)庫索引的應(yīng)用有哪些優(yōu)秀的姿勢呢氯夷?
MongoDB 索引類型
單鍵索引
db.user.createIndex({createdAt: 1})
createdAt
創(chuàng)建了單字段索引,可以快速檢索createdAt
字段的各種查詢請求靶擦,比較常見
{createdAt: 1}
升序索引腮考,也可以通過{createdAt: -1}
來降序索引雇毫,對于單字段索引,
升序/降序效果是一樣的踩蔚。
組合索引
db.user.createIndex({age: 1, createdAt: 1})
可以對多個字段聯(lián)合創(chuàng)建索引棚放,先按第一個字段排序,第一個字段相同的文檔按第二個字段排序馅闽,依次類推飘蚯,所以在做查詢的時候排序與索引的應(yīng)用也是非常重要。
實際場景福也,使用最多的也是這類索引局骤,在MongoDB中是滿足所以能匹配符合索引前綴的查詢,例如已經(jīng)存在db.user.createIndex({age: 1, createdAt: 1})
拟杉,
我們就不需要單獨為db.user.createIndex({age: 1})
建立索引庄涡,因為單獨使用age做查詢條件的時候,也是可以命中db.user.createIndex({age: 1, createdAt: 1})
搬设,但是使用createdAt
單獨作為查詢條件的時候是不能匹配db.user.createIndex({age: 1, createdAt: 1})
多值索引
當(dāng)索引的字段為數(shù)組時穴店,創(chuàng)建出的索引稱為多key索引,多key索引會為數(shù)組的每個元素建立一條索引
// 用戶的社交登錄信息拿穴,
schema = {
…
snsPlatforms:[{
platform:String, // 登錄平臺
openId:String, // 登錄唯一標(biāo)識符
}]
}
// 這也是一個列轉(zhuǎn)行文檔設(shè)計泣洞,后面會說
db.user.createIndex({snsPlatforms.openId:1})
TTL 索引
可以針對某個時間字段,指定文檔的過期時間(用于僅在一段時間有效的數(shù)據(jù)存儲默色,文檔達(dá)到指定時間就會被刪除球凰,這樣就可以完成自動刪除數(shù)據(jù))
這個刪除操作是安全的,數(shù)據(jù)會選擇在應(yīng)用的低峰期執(zhí)行腿宰,所以不會因為刪除大量文件造成高額IO嚴(yán)重影響數(shù)據(jù)性能呕诉。
部分索引
3.2版本
才支持該特性,給符合條件的數(shù)據(jù)文檔建立索引吃度,意在節(jié)約索引存儲空間與寫入成本
db.user.createIndex({sns.qq.openId:1})
/**
* 給qq登錄openid加索引甩挫,系統(tǒng)其實只有很少一部分用到qq登錄 ,然后才會存在這個數(shù)據(jù)字段椿每,這個時
* 候就沒有必要給所有文檔加上這個索引伊者,僅需要滿足條件才加索引
*/
db.user.createIndex({sns.qq.openId:1} ,{partialFilterExpression:{$exists:1}})
稀疏索引
稀疏索引僅包含具有索引字段的文檔條目,即使索引字段包含空值也是如此间护。
索引會跳過缺少索引字段的所有文檔亦渗。
db.user.createIndex({sns.qq.openId:1} ,{sparse:true})
注:3.2版本開始,提供了部分索引汁尺,可以當(dāng)做稀疏索引的超集法精,官方推薦優(yōu)先使用部分索引而不是稀疏索引。
ESR索引規(guī)則
索引字段順序: equal(精準(zhǔn)匹配) > sort (排序條件)> range (范圍查詢)
精確(Equal)
匹配的字段放最前面,排序(Sort)
條件放中間,范圍(Range)
匹配的字段放最后面,也適用于ES,ER。
實際例子:獲取成績表中亿虽,高2班中數(shù)學(xué)分?jǐn)?shù)大于120的學(xué)生菱涤,按照分?jǐn)?shù)從大到小排序
不難看出,班級和學(xué)科(數(shù)學(xué))可以是精準(zhǔn)匹配洛勉,分?jǐn)?shù)是一個范圍查詢粘秆,同時也是排序條件
那么按照ESR規(guī)則我們可以這樣建立索引
{"班級":1,"學(xué)科":1,"分?jǐn)?shù)":1}
我們怎么分析這個索引的命中與有效情況呢?
db.collection.explain()
函數(shù)可以輸出文檔查找執(zhí)行計劃收毫,可以幫助我們做更正確的選擇攻走。
分析函數(shù)返回的數(shù)據(jù)很多,但我們主要可以關(guān)注這個字段
executionStats 執(zhí)行統(tǒng)計
{
"queryPlanner": {
"plannerVersion": 1,
"namespace": "test.user",
"indexFilterSet": false,
"parsedQuery": {
"age": {
"$eq": 13
}
},
"winningPlan": { ... },
"rejectedPlans": []
},
"executionStats": {
"executionSuccess": true,
"nReturned": 100,
"executionTimeMillis": 137,
"totalKeysExamined": 48918,
"totalDocsExamined": 48918,
"allPlansExecution": []
},
"ok": 1,
}
nReturned
實際返回數(shù)據(jù)行數(shù)
executionTimeMillis
命令執(zhí)行總時間,單位毫秒
totalKeysExamined
表示MongoDB 掃描了N個索引數(shù)據(jù)此再。 檢查的鍵數(shù)與返回的文檔數(shù)相匹配昔搂,這意味著mongod只需檢查索引鍵即可返回結(jié)果。mongod不必掃描所有文檔输拇,只有N個匹配的文檔被拉入內(nèi)存摘符。 這個查詢結(jié)果是非常高效的。
totalDocsExamined
文檔掃描數(shù)
這幾個字段的值越小說明效率越好策吠,最佳狀態(tài)是
nReturned
= totalKeysExamined
= totalDocsExamined
如果相差很大逛裤,說明還有很大優(yōu)化空間,當(dāng)具體業(yè)務(wù)還要酌情分析猴抹。
查詢優(yōu)化器針對該query所返回的最優(yōu)執(zhí)行計劃的詳細(xì)內(nèi)容(queryPlanne.winningPlan)
stage
COLLSCAN:全表掃描,這個情況是最糟糕的
IXSCAN:索引掃描
FETCH:根據(jù)索引去檢索指定document
SHARD_MERGE:將各個分片返回數(shù)據(jù)進(jìn)行merge
SORT:表明在內(nèi)存中進(jìn)行了排序
LIMIT:使用limit限制返回數(shù)
SKIP:使用skip進(jìn)行跳過
IDHACK:針對_id進(jìn)行查詢
SHARDING_FILTER:通過mongos對分片數(shù)據(jù)進(jìn)行查詢
COUNT:利用db.coll.explain().count()之類進(jìn)行count運算
COUNTSCAN: count不使用Index進(jìn)行count時的stage返回
COUNT_SCAN: count使用了Index進(jìn)行count時的stage返回
SUBPLA:未使用到索引的$or查詢的stage返回
TEXT:使用全文索引進(jìn)行查詢時候的stage返回
PROJECTION:限定返回字段時候stage的返回
我們不希望看到的(出現(xiàn)以下情況带族,就要注意了,問題可能就出現(xiàn)了)
COLLSCAN(全表掃描)
SORT但是沒有相關(guān)的索引
超大的SKIP
SUBPLA在使用$or的時候沒有命中索引
COUNTSCAN 執(zhí)行count沒有命中索引
然后是我們看看一條普通查詢實際執(zhí)行順序
db.user.find({age:13}).skip(100).limit(100).sort({createdAt:-1})
圖中可以看出蟀给,首先是IXSCAN索引掃描蝙砌,最后是SKIP跳過數(shù)據(jù)進(jìn)行過濾。
在executionStats每一個項都有nReturned 與 executionTimeMillisEstimate跋理,這樣我們可以由內(nèi)向外查看整個查詢執(zhí)行情況择克,在哪一步出現(xiàn)執(zhí)行慢的問題。
關(guān)于列轉(zhuǎn)行文檔設(shè)計模式
首先數(shù)據(jù)庫索引并不是越多越好前普,在MongoDB單文檔索引上限祠饺,集合中索引不能超過64個,一些知名大廠推薦不超過10個。
而在一個主表中汁政,由于冗余文檔設(shè)計,就會存在非常多信息需要增加索引缀旁,我們還是以社交登錄為例子
常規(guī)設(shè)計
schema = {
…
qq:{
openId:String
},
wxapp:{
openId:String,
},
weibo:{
openId:String,
}
…
}
// 每次增加新的登錄類型记劈,需要修改文檔schema和增加索引
db.user.createIndex({qq.openId:1})
db.user.createIndex({wxapp.openId:1})
db.user.createIndex({weibo.openId:1})
列轉(zhuǎn)行設(shè)計
schema = {
…
snsPlatforms:[{
platform:String, // 登錄平臺
openId:String, // 登錄唯一標(biāo)識符
}]
}
// 此時無論是新增登錄平臺還是刪除,都不需要變更索引設(shè)計,一個索引解決所有同類型問題
db.user.createIndex({snsPlatforms.openId:1,snsPlatforms.platform:1})
提問:為什么openId要放在plaform前面呢并巍?
這個小故事講述了小李在遇到自身知識不能解決的問題目木,然后事情的處理思路與過程。每個人都有自己能力所不及的地方,那么這種情況要優(yōu)先解決問題刽射,或者降低事故的影響范圍军拟。