千萬級 MongoDB 數(shù)據(jù)索引優(yōu)化實踐

image.png

小李是這家公司的后端負(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})
image

圖中可以看出蟀给,首先是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)先解決問題刽射,或者降低事故的影響范圍军拟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市誓禁,隨后出現(xiàn)的幾起案子懈息,更是在濱河造成了極大的恐慌,老刑警劉巖摹恰,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辫继,死亡現(xiàn)場離奇詭異,居然都是意外死亡俗慈,警方通過查閱死者的電腦和手機(jī)姑宽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闺阱,“玉大人炮车,你說我怎么就攤上這事『ɡ#” “怎么了瘦穆?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長救拉。 經(jīng)常有香客問我难审,道長,這世上最難降的妖魔是什么亿絮? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任告喊,我火速辦了婚禮,結(jié)果婚禮上派昧,老公的妹妹穿的比我還像新娘黔姜。我一直安慰自己,他們只是感情好蒂萎,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布秆吵。 她就那樣靜靜地躺著,像睡著了一般五慈。 火紅的嫁衣襯著肌膚如雪纳寂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天泻拦,我揣著相機(jī)與錄音毙芜,去河邊找鬼。 笑死争拐,一個胖子當(dāng)著我的面吹牛腋粥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼隘冲,長吁一口氣:“原來是場噩夢啊……” “哼闹瞧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起展辞,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤奥邮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后纵竖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漠烧,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年靡砌,在試婚紗的時候發(fā)現(xiàn)自己被綠了已脓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡通殃,死狀恐怖度液,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情画舌,我是刑警寧澤堕担,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站曲聂,受9級特大地震影響霹购,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朋腋,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一齐疙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旭咽,春花似錦贞奋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仲墨,卻和暖如春勾缭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背目养。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工漫拭, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人混稽。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親匈勋。 傳聞我的和親對象是個殘疾皇子礼旅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359