MongoDB索引優(yōu)化詳解

索引基礎(chǔ)知識(shí)

什么是索引

索引最常用的比喻就是書籍的目錄,查詢索引就像查詢一本書的目錄。本質(zhì)上目錄是將書中一小部分內(nèi)容信息(比如題目)和內(nèi)容的位置信息(頁碼)共同構(gòu)成,而由于信息量小(只有題目),所以我們可以很快找到我們想要的信息片段,再根據(jù)頁碼找到相應(yīng)的內(nèi)容。同樣索引也是只保留某個(gè)域的一部分信息(建立了索引的field的信息),以及對(duì)應(yīng)的文檔的位置信息。
假設(shè)我們有如下文檔(每行的數(shù)據(jù)在MongoDB中是存在于一個(gè)Document當(dāng)中)

姓名 id 部門 city score
張三 2 xxx Beijing 90
李四 1 xxx Shanghai 70
王五 3 xxx guangzhou 60

假如我們想找id為2的document(即張三的記錄),如果沒有索引,我們就需要掃描整個(gè)數(shù)據(jù)表,然后找出所有為2的document。當(dāng)數(shù)據(jù)表中有大量documents的時(shí)候,這個(gè)時(shí)間就會(huì)非常長(從磁盤上查找數(shù)據(jù)還涉及大量的IO操作)。建立索引后會(huì)有什么變化呢?MongoDB會(huì)將id數(shù)據(jù)拿出來建立索引數(shù)據(jù),如下

索引值 位置
1 pos2
2 pos1
3 pos3

這樣我們就可以通過掃描這個(gè)小表找到document對(duì)應(yīng)的位置。

查找過程示意圖如下:


圖片來源MongoDB官網(wǎng)

為什么這樣速度會(huì)快呢?這主要有幾方面的因素

  1. 索引數(shù)據(jù)通過B樹來存儲(chǔ),從而使得搜索的時(shí)間復(fù)雜度為O(logdN)級(jí)別的(d是B樹的度, 通常d的值比較大,比如大于100),比原先O(N)的復(fù)雜度大幅下降。這個(gè)差距是驚人的,以一個(gè)實(shí)際例子來看,假設(shè)d=100,N=1億,那么O(logdN) = 8, 而O(N)是1億。是的,這就是算法的威力。
  2. 索引本身是在高速緩存當(dāng)中,相比磁盤IO操作會(huì)有大幅的性能提升。(需要注意的是,有的時(shí)候數(shù)據(jù)量非常大的時(shí)候,索引數(shù)據(jù)也會(huì)非常大,當(dāng)大到超出內(nèi)存容量的時(shí)候,會(huì)導(dǎo)致部分索引數(shù)據(jù)存儲(chǔ)在磁盤上,這會(huì)導(dǎo)致磁盤IO的開銷大幅增加,從而影響性能,所以務(wù)必要保證有足夠的內(nèi)存能容下所有的索引數(shù)據(jù))

當(dāng)然,事物總有其兩面性,在提升查詢速度的同時(shí),由于要建立索引,所以寫入操作時(shí)就需要額外的添加索引的操作,這必然會(huì)影響寫入的性能,所以當(dāng)有大量寫操作而讀操作比較少的時(shí)候,且對(duì)讀操作性能不需要考慮的時(shí)候,就不適合建立索引。當(dāng)然,目前大多數(shù)互聯(lián)網(wǎng)應(yīng)用都是讀操作遠(yuǎn)大于寫操作,因此建立索引很多時(shí)候是非常劃算和必要的操作。

關(guān)于索引原理的詳細(xì)解釋可以參考文章MySQL索引背后的數(shù)據(jù)結(jié)構(gòu)及算法原理,雖然講得是MySQL但是原理相似。

MongoDB有哪些類型的索引

單字段索引 (Single Field Index)

這個(gè)是最簡(jiǎn)單最常用的索引類型,比如我們上邊的例子,為id建立一個(gè)單獨(dú)的索引就是此種類型。

 # 為id field建立索引,1表示升序,-1表示降序,沒有差別
db.employee.createIndex({'id': 1})

需要注意的是通常MongoDB會(huì)自動(dòng)為我們的文檔插入'_id' field,且已經(jīng)按照升序進(jìn)行索引,如果我們插入的文檔中包含有'_id' field,則MongoDB就不會(huì)自動(dòng)創(chuàng)建'_id' field,但是需要我們自己來保證唯一性從而唯一標(biāo)識(shí)一個(gè)文檔

復(fù)合索引 (Compound Index)

符合索引的原理如下圖所示:


復(fù)合索引示意圖

上圖查詢索引的時(shí)候會(huì)先查詢userid,再查詢score,然后就可以找到對(duì)應(yīng)的文檔。
對(duì)于復(fù)合索引需要注意以下幾點(diǎn):

索引field的先后順序很關(guān)鍵,影響有兩方面:

  1. MongoDB在復(fù)合索引中是根據(jù)prefix排序查詢,就是說排在前面的可以單獨(dú)使用。我們創(chuàng)建一個(gè)如下的索引
db.collection.createIndex({'id': 1, 'city': 1, 'score': 1}) 

我們?nèi)缦碌牟樵兛梢岳盟饕?/p>

db.collection.find({'id': xxx})
db.collection.find({'id': xxx, 'city': xxx})
db.collection.find({'id': xxx, 'city':xxx, 'score': xxxx})

但是如下的查詢無法利用該索引

db.collection.find({'city': xxx})
db.collection.find({'city':xxx, 'score': xxxx})

還有一種特殊的情況,就是如下查詢:

db.collection.find({'id': xxx, 'score': xxxx})

這個(gè)查詢也可以利用索引的前綴'id'來查詢,但是卻不能針對(duì)score進(jìn)行查詢,你可以說是部分利用了索引,因此其效率可能不如如下索引:

db.collection.createIndex({'id': 1, 'score': 1}) 

2.過濾出的document越少的field越應(yīng)該放在前面,比如此例中id如果是唯一的,那么就應(yīng)該放在最前面,因?yàn)檫@樣通過id就可以鎖定唯一一個(gè)文檔。而如果通過city或者score過濾完成后還是會(huì)有大量文檔,這就會(huì)影響最終的性能。

索引的排序順序不同

復(fù)合索引最末尾的field,其排序順序不同對(duì)于MongoDB的查詢排序操作是有影響的。
比如:

db.events.createIndex( { username: 1, date: -1 } )

這種情況下, 如下的query可以利用索引:

db.events.find().sort( { username: 1, date: -1 } )

但是如下query則無法利用index進(jìn)行排序

db.events.find().sort( { username: 1, date: 1 } )

多key索引 (Multikey Index)

這個(gè)主要是針對(duì)數(shù)據(jù)類型為數(shù)組的類型,如下示例:

{"name" : "jack", "age" : 19, habbit: ["football, runnning"]}
db.person.createIndex( {habbit: 1} )  // 自動(dòng)創(chuàng)建多key索引
db.person.find( {habbit: "football"} )

其它類型索引

另外,MongoDB中還有其它如哈希索引,地理位置索引以及文本索引,主要用于一些特定場(chǎng)景,具體可以參考官網(wǎng),在此不再詳解

索引屬性

索引主要有以下幾個(gè)屬性:

  • unique:這個(gè)非常常用,用于限制索引的field是否具有唯一性屬性,即保證該field的值唯一
  • partial:很有用,在索引的時(shí)候只針對(duì)符合特定條件的文檔來建立索引,如下
db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } } //只有當(dāng)rating大于5時(shí)才會(huì)建立索引
)

這樣做的好處是,我們可以只為部分?jǐn)?shù)據(jù)建立索引,從而可以減少索引數(shù)據(jù)的量,除節(jié)省空間外,其檢索性能也會(huì)因?yàn)檩^少的數(shù)據(jù)量而得到提升。

  • sparse:可以認(rèn)為是partial索引的一種特殊情況,由于MongoDB3.2之后已經(jīng)支持partial屬性,所以建議直接使用partial屬性。
  • TTL。 可以用于設(shè)定文檔有效期,有效期到自動(dòng)刪除對(duì)應(yīng)的文檔。

通過explain結(jié)果來分析性能

我們往往會(huì)通過打點(diǎn)數(shù)據(jù)來分析業(yè)務(wù)的性能瓶頸,這時(shí),我們會(huì)發(fā)現(xiàn)很多瓶頸都是出現(xiàn)在數(shù)據(jù)庫相關(guān)的操作上,這時(shí)由于數(shù)據(jù)庫的查詢和存取都涉及大量的IO操作,而且有時(shí)由于使用不當(dāng),會(huì)導(dǎo)致IO操作的大幅度增長,從而導(dǎo)致了產(chǎn)生性能問題。而MongoDB提供了一個(gè)explain工具來用于分析數(shù)據(jù)庫的操作。直接拿官網(wǎng)的示例來做說明:

假設(shè)我們?cè)趇nventory collection中有如下文檔:

{ "_id" : 1, "item" : "f1", type: "food", quantity: 500 }
{ "_id" : 2, "item" : "f2", type: "food", quantity: 100 }
{ "_id" : 3, "item" : "p1", type: "paper", quantity: 200 }
{ "_id" : 4, "item" : "p2", type: "paper", quantity: 150 }
{ "_id" : 5, "item" : "f3", type: "food", quantity: 300 }
{ "_id" : 6, "item" : "t1", type: "toys", quantity: 500 }
{ "_id" : 7, "item" : "a1", type: "apparel", quantity: 250 }
{ "_id" : 8, "item" : "a2", type: "apparel", quantity: 400 }
{ "_id" : 9, "item" : "t2", type: "toys", quantity: 50 }
{ "_id" : 10, "item" : "f4", type: "food", quantity: 75 }

假設(shè)此時(shí)沒有建立索引,做如下查詢:

db.inventory.find( { quantity: { $gte: 100, $lte: 200 } } )

返回結(jié)果如下:

{ "_id" : 2, "item" : "f2", "type" : "food", "quantity" : 100 }
{ "_id" : 3, "item" : "p1", "type" : "paper", "quantity" : 200 }
{ "_id" : 4, "item" : "p2", "type" : "paper", "quantity" : 150 }

這是我們可以通過explain來分析整個(gè)查詢的過程:

# explain 有三種模式: "queryPlanner", "executionStats", and "allPlansExecution".
# 其中最常用的就是第二種"executionStats",它會(huì)返回具體執(zhí)行的時(shí)候的統(tǒng)計(jì)數(shù)據(jù)
db.inventory.find(
   { quantity: { $gte: 100, $lte: 200 } }
).explain("executionStats")

explain的結(jié)果如下:

{
   "queryPlanner" : {
         "plannerVersion" : 1,
         ...
         "winningPlan" : {
            "stage" : "COLLSCAN",
            ...
         }
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 3,  # 查詢返回的document數(shù)量
      "executionTimeMillis" : 0, # 執(zhí)行查詢所用的時(shí)間
      "totalKeysExamined" : 0, # 總共查詢了多少個(gè)key,由于沒有使用索引,因此這里為0
      "totalDocsExamined" : 10, # 總共在磁盤查詢了多少個(gè)document,由于是全表掃描,我們總共有10個(gè)documents,因此,這里為10
      "executionStages" : {
         "stage" : "COLLSCAN",  # 注意這里,"COLLSCAN"意味著全表掃描
         ...
      },
      ...
   },
   ...
}

上面的結(jié)果中有一個(gè)"stage"字段,上例中stage為"COLLSCAN",而MongoDB總共有如下幾種stage:

  • COLLSCAN – Collection scan
  • IXSCAN – Scan of data in index keys
  • FETCH – Retrieving documents
  • SHARD_MERGE – Merging results from shards
  • SORT – Explicit sort rather than using index order

現(xiàn)在我們來創(chuàng)建一個(gè)索引:

db.inventory.createIndex( { quantity: 1 } )

再來看下explain的結(jié)果

db.inventory.find(
   { quantity: { $gte: 100, $lte: 200 } }
).explain("executionStats")

結(jié)果如下:

{
   "queryPlanner" : {
         "plannerVersion" : 1,
         ...
         "winningPlan" : {
               "stage" : "FETCH",
               "inputStage" : {
                  "stage" : "IXSCAN",  # 這里"IXSCAN"意味著索引掃描
                  "keyPattern" : {
                     "quantity" : 1
                  },
                  ...
               }
         },
         "rejectedPlans" : [ ]
   },
   "executionStats" : {
         "executionSuccess" : true,
         "nReturned" : 3,
         "executionTimeMillis" : 0,
         "totalKeysExamined" : 3,  # 這里nReturned、totalKeysExamined和totalDocsExamined相等說明索引沒有問題,因?yàn)槲覀兺ㄟ^索引快速查找到了三個(gè)文檔,且從磁盤上也是去取這三個(gè)文檔,并返回三個(gè)文檔。
         "totalDocsExamined" : 3,
         "executionStages" : {
            ...
         },
         ...
   },
   ...
}

再來看下如何通過explain來比較compound index的性能,之前我們?cè)诮榻B復(fù)合索引的時(shí)候已經(jīng)說過field的順序會(huì)影響查詢的效率。有時(shí)這種順序并不太好確定(比如field的值都不是unique的),那么怎么判斷哪種順序的復(fù)合索引的效率高呢,這就像需要explain結(jié)合hint來進(jìn)行分析。
比如我們要做如下查詢:

db.inventory.find( {
   quantity: {
      $gte: 100, $lte: 300
   },
   type: "food"
} )

會(huì)返回如下文檔:

{ "_id" : 2, "item" : "f2", "type" : "food", "quantity" : 100 }
{ "_id" : 5, "item" : "f3", "type" : "food", "quantity" : 300 }

現(xiàn)在我們要比較如下兩種復(fù)合索引

db.inventory.createIndex( { quantity: 1, type: 1 } )
db.inventory.createIndex( { type: 1, quantity: 1 } )

分析索引 { quantity: 1, type: 1 }的情況

# 結(jié)合hint和explain來進(jìn)行分析
db.inventory.find(
   { quantity: { $gte: 100, $lte: 300 }, type: "food" }
).hint({ quantity: 1, type: 1 }).explain("executionStats") # 這里使用hint會(huì)強(qiáng)制數(shù)據(jù)庫使用索引 { quantity: 1, type: 1 }

explain結(jié)果

{
   "queryPlanner" : {
      ...
      "winningPlan" : {
         "stage" : "FETCH",
         "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
               "quantity" : 1,
               "type" : 1
            },
            ...
            }
         }
      },
      "rejectedPlans" : [ ]
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 2,
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 5,  # 這里是5與totalDocsExamined、nReturned都不相等
      "totalDocsExamined" : 2,
      "executionStages" : {
      ...
      }
   },
   ...
}

再來看下索引 { type: 1, quantity: 1 } 的分析

db.inventory.find(
   { quantity: { $gte: 100, $lte: 300 }, type: "food" }
).hint({ type: 1, quantity: 1 }).explain("executionStats")

結(jié)果如下:

{
   "queryPlanner" : {
      ...
      "winningPlan" : {
         "stage" : "FETCH",
         "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
               "type" : 1,
               "quantity" : 1
            },
            ...
         }
      },
      "rejectedPlans" : [ ]
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 2,
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 2, # 這里是2,與totalDocsExamined、nReturned相同
      "totalDocsExamined" : 2,
      "executionStages" : {
         ...
      }
   },
   ...
}

可以看出后一種索引的totalKeysExamined返回是2,相比前一種索引的5,顯然更有效率。

References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,432評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容