歡迎光臨
每天分享高質量文章

MongoDb最佳化指南

作者:吳紋羽

連結:https://www.cnblogs.com/mokafamily/p/4102829.html

https://www.cnblogs.com/mokafamily/p/4076954.html

1、為什麼選擇MongoDB?

1、效能

在大資料時代中,大資料量的處理已經成了考量一個資料庫最重要的原因之一。而MongoDB的一個主要標的就是盡可能的讓資料庫保持卓越的效能,這很大程度地決定了MongoDB的設計。在一個以傳統機械硬碟為主導的年代,硬碟很可能會成為效能的短板,而MongoDB選擇了最大程度而利用記憶體資源用作快取來換取卓越的效能,並且會自動選擇速度最快的索引來進行查詢。MongoDB盡可能精簡資料庫,將盡可能多的操作交給客戶端,這種方式也是MongoDB能夠保持卓越效能的原因之一。

2、擴充套件

現在網際網路的資料量已經從過去的MB、GB變為了現在的TB級別,單一的資料庫顯然已經無法承受,擴充套件性成為重要的話題,然而現在的開發人員常常在選擇擴充套件方式的時候犯了難,到底是選擇橫向擴充套件還是縱向擴充套件呢?

橫向擴充套件(scale out)以增加分割槽的方式將資料庫拆分成不同的區塊來分佈到不同的機器中來,這樣的優勢是擴充套件成本低但管理困難。

縱向擴充套件(scale up) 縱向擴充套件與橫向擴充套件不同的是他會將原本的伺服器進行升級,讓其擁有更強大的計算能力。這樣的優勢是易於管理無需考慮擴充套件帶來的眾多問題,但缺點也顯而易見,那就是成本高。一臺大型機的價格往往非常昂貴,並且這樣的升級在資料達到極限時,可能就找不到計算能力更為強大的機器了。

而MongoDB選擇的是更為經濟的橫向擴充套件,他可以很容易的將資料拆分至不同的伺服器中。而且在獲取資料時開發者也無需考慮多伺服器帶來的問題,MongoDB可以將開發者的請求自動路由到正確的伺服器中,讓開發者脫離橫向擴充套件帶來的弊病,更專註於程式的開發上。

3、使用

MongoDB採用的是NoSQL的設計方式,可以更加靈活的運算元據。在進行傳統的RDBMS中你一定遇到過幾十行甚至上百行的複雜SQL陳述句,傳統的RDBMS的SQL陳述句中包含著大量關聯,子查詢等陳述句,在增加複雜性的同時還讓效能調優變得更加困難。MongoDB的面向檔案(document-oriented)設計中採用更為靈活的檔案來作為資料模型用來取代RDBMS中的行,面向檔案的設計讓開發人員獲取資料的方式更加靈活,甚至於開發人員僅用一條陳述句即可查詢複雜的巢狀關係,讓開發人員不必為了獲取資料而絞盡腦汁。

2、NoSQL對傳統資料庫設計思維的影響

1、預設計樣式與動態樣式

傳統資料庫設計思維中,專案的設計階段需要對資料庫表中的欄位名稱、欄位型別、進行規定,如果嘗試插入不符合設計的資料,資料庫不會接受這條資料以保證資料的完整性。

--資料庫欄位:NAME, SONG

INSERT INTO T_INFO VALUES('John','Come Together');  --成功

INSERT INTO T_INFO VALUES('小明'20'xiaoming@111.com');  --失敗

NoSQL採用的是對集合(類似”表”)中的檔案(類似於”行”)進行動態追加,在建立集合之初不會對資料型別進行限定,任何檔案都可以追加到任何集合中去,例如我們可以將這樣兩條檔案新增到一個集合中去:

{"name" : "John""song" : "Come Together"}

{"name" : "小明",  "age":"20""email" : "xiaoming@111.com"}

MongoDB中檔案的格式類似於我們常見的JSON,由此可見,我們第一個擁有”name”、”song”兩個欄位,而第二個擁有”name”、”age”、”email”三個欄位,這在預設計樣式中的資料庫是不可能插入成功的,但在MongoDB的動態樣式是可以的,這樣做的優勢是我們不必為一些數量很少,但種類很多的欄位單獨設計一張表,可以將他們集中在單獨一張表進行儲存,但這樣做的弊病也是顯而易見的,我們在獲取資料時需要對同一張表的不同檔案進行區分,增加了開發上的程式碼量。所以在設計之初需要權衡動態樣式的優劣來選擇表中的資料型別。

2、正規化化與反正規化化

正規化化(normalization)是關係模型的發明者埃德加·科德於1970年提出這一概念,正規化化會將資料分散到不同的表中,利用關係模型進行關聯,由此帶來的優點是,在後期進行修改時,不會影響到與其關聯的資料,僅對自身修改即可完成。

反正規化化(denormalization)是針對正規化化提出的相反理念,反正規化化會將當前檔案的資料集中存放在本表中,而不會採用拆分的方式進行儲存。

正規化化和反正規化化之間不存在優劣的問題,正規化化的好處是可以在我們寫入、修改、刪除時的提供更高效能,而反正規化化可以提高我們在查詢時的效能。當然NoSQL中是不存在關聯查詢的,以此提高查詢效能,但我們依舊可以以在表中儲存關聯表ID的方式進行正規化化。但由此可見,NoSQL的理念中反正規化化的地位是大於正規化化的。

3、效能與使用者量

“如何能讓軟體擁有更高的效能?”,我想這是一個大部分開發者都思考過的問題。效能往往決定了一個軟體的質量,如果你開發的是一個網際網路產品,那麼你的產品效能將更加受到考驗,因為你面對的是廣大的網際網路使用者,他們可不是那麼有耐心的。嚴重點說,頁面的載入速度每增加一秒也許都會使你失去一部分使用者,也就是說,載入速度和使用者量是成反比的。那麼使用者能夠接受的載入速度到底是多少呢?  

如圖,如果頁面載入時間超過10s那麼使用者就會離開,如果1s–10s的話就需要有提示,但如果我們的頁面沒有提示的話需要多快的載入速度呢?是的,1s 。

當然,這是站在一個產品經理的角度來說的,但如果站在一個技術人員的角度來說呢?載入速度和使用者量就是成正比的,你的使用者數量越多需要處理的資料當然也就越多,載入速度當然也就越慢。這是一件很有趣的事,所以如果你的產品如果是一件激動人心的產品,那麼作為技術人員你需要做的事就是讓軟體的效能和使用者的數量同時增長,甚至效能增長要快於使用者量的增長。

資料庫效能對軟體整體效能的影響是不言而喻的,那麼,當我們使用MongoDB時改如何提高資料庫效能呢?

4、正規化化與反正規化化

在專案設計階段,明確集合的用途是對效能調優非常重要的一步。

從效能最佳化的角度來看,集合的設計我們需要考慮的是集合中資料的常用操作,例如我們需要設計一個日誌(log)集合,日誌的檢視頻率不高,但寫入頻率卻很高,那麼我們就可以得到這個集合中常用的操作是更新(增刪改)。如果我們要儲存的是城市串列呢?顯而易見,這個集合是一個檢視頻率很高,但寫入頻率很低的集合,那麼常用的操作就是查詢。

對於頻繁更新和頻繁查詢的集合,我們最需要關註的重點是他們的正規化化程度,在上篇正規化化與反正規化化的介紹中我們瞭解到,正規化化與反正規化化的合理運用對於效能的提高至關重要。然而這種設計的使用非常靈活,假設現在我們需要儲存一篇圖書及其作者,在MongoDB中的關聯就可以體現為以下幾種形式:

1、完全分離(正規化化設計)

示例1:

{
     "_id" : ObjectId("5124b5d86041c7dca81917"),
     "title" : "如何使用MongoDB"
      "author" : [ 
               ObjectId("144b5d83041c7dca84416"),
              ObjectId("144b5d83041c7dca84418"),
              ObjectId("144b5d83041c7dca84420"),
     ]
 }

我們將作者(comment) 的id陣列作為一個欄位新增到了圖書中去。這樣的設計方式是在非關係型資料庫中常用的,也就是我們所說的正規化化設計。在MongoDB中我們將與主鍵沒有直接關係的圖書單獨提取到另一個集合,用儲存主鍵的方式進行關聯查詢。當我們要查詢文章和評論時需要先查詢到所需的文章,再從文章中獲取評論id,最後用獲得的完整的文章及其評論。在這種情況下查詢效能顯然是不理想的。但當某位作者的資訊需要修改時,正規化化的維護優勢就凸顯出來了,我們無需考慮此作者關聯的圖書,直接進行修改此作者的欄位即可。

2、完全內嵌(反正規化化設計)

示例2:

{
       "_id" : ObjectId("5124b5d86041c7dca81917"),
       "title" : "如何使用MongoDB",
       "author" : [
                {
                         "name" : "丁磊"
                         "age" : 40,
                         "nationality" : "china",
                },
                {
                         "name" : "馬雲"
                         "age" : 49,
                         "nationality" : "china",
                },
                {
                         "name" : "張召忠"
                         "age" : 59,
                         "nationality" : "china",
                },
      ]
  }

在這個示例中我們將作者的欄位完全嵌入到了圖書中去,在查詢的時候直接查詢圖書即可獲得所對應作者的全部資訊,但因一個作者可能有多本著作,當修改某位作者的資訊時時,我們需要遍歷所有圖書以找到該作者,將其修改。

3、部分內嵌(折中方案)

示例3:

{
       "_id" : ObjectId("5124b5d86041c7dca81917"),
       "title" : "如何使用MongoDB",
       "author" : [ 
               {
                         "_id" : ObjectId("144b5d83041c7dca84416"),
                         "name" : "丁磊"
                },
                {
                         "_id" : ObjectId("144b5d83041c7dca84418"),
                         "name" : "馬雲"
                },
                {
                         "_id" : ObjectId("144b5d83041c7dca84420"),
                         "name" : "張召忠"
                },
      ]
  }

這次我們將作者欄位中的最常用的一部分提取出來。當我們只需要獲得圖書和作者名時,無需再次進入作者集合進行查詢,僅在圖書集合查詢即可獲得。

這種方式是一種相對折中的方式,既保證了查詢效率,也保證的更新效率。但這樣的方式顯然要比前兩種較難以掌握,難點在於需要與實際業務進行結合來尋找合適的提取欄位。如同示例3所述,名字顯然不是一個經常修改的欄位,這樣的欄位如果提取出來是沒問題的,但如果提取出來的欄位是一個經常修改的欄位(比如age)的話,我們依舊在更新這個欄位時需要大範圍的尋找並依此進行更新。

 

在上面三個示例中,第一個示例的更新效率是最高的,但查詢效率是最低的,而第二個示例的查詢效率最高,但更新效率最低。所以在實際的工作中我們需要根據自己實際的需要來設計表中的欄位,以獲得最高的效率。

5、理解填充因子

何為填充因子?

填充因子(padding factor)是MongoDB為檔案的擴充套件而預留的增長空間,因為MongoDB的檔案是以順序表的方式儲存的,每個檔案之間會非常緊湊,如圖所示。

  (註:圖片出處:《MongoDB The Definitive Guide》)

  

1.元素之間沒有多餘的可增長空間。

2.當我們對順序表中某個元素的大小進行增長的時候,就會導致原來分配的空間不足,只能要求其向後移動。

3.當修改元素移動後,後續插入的檔案都會提供一定的填充因子,以便於檔案頻繁的修改,如果沒有不再有檔案因增大而移動的話,後續插入的檔案的填充因子會依此減小。

填充因子的理解之所以重要,是因為檔案的移動非常消耗效能,頻繁的移動會大大增加系統的負擔,在實際開發中最有可能會讓檔案體積變大的因素是陣列,所以如果我們的檔案會頻繁修改並增大空間的話,則一定要充分考慮填充因子。

那麼如果我們的檔案是個常常會擴充套件的話,應該如何提高效能?

兩種方案

1、增加初始分配空間。在集合的屬性中包含一個 usePowerOf2Sizes 屬性,當這個選項為true時,系統會將後續插入的檔案,初始空間都分配為2的N次方。

這種分配機制適用於一個資料會頻繁變更的集合使用,他會給每個檔案留有更大的空間,但因此空間的分配不會像原來那樣高效,如果你的集合在更新時不會頻繁的出現移動現象,這種分配方式會導致寫入速度相對變慢。

2、我們可以利用資料強行將初始分配空間擴大。

db.book.insert({
    "name" : "MongoDB",
    "publishing" : "清華大學出版社",
    "author" : "john"
    "tags" : []
    "stuff" : "ggggggggggggggggggggggggggggggggggggg
               ggggggggggggggggggggggggggggggggggggg
               ggggggggggggggggggggggggggggggggggggg"

})

是的,這樣看起來可能不太優雅…但有時卻很有效!當我們對這個檔案進行增長式修改時,只要將stuff欄位刪掉即可。當然,這個stuff欄位隨便你怎麼起名,包括裡邊的填充字元當然也是可以隨意新增的。  

6、準確利用索引

索引對於一個資料庫的影響相信大家一定瞭解,如果一個查詢命令進入到資料庫中後,查詢最佳化器沒有找到合適的索引,那麼資料庫會進行全集合掃描(在RDBMS中也叫全表掃描),全集合查詢對於效能的影響是災難性的。

沒有索引的查詢就如同在詞典那毫無規律的海量詞彙中獲得某個你想要的詞彙,但這個詞典是沒有目錄的,只能透過逐頁來查詢。這樣的查詢可能會讓你耗費幾個小時的時間,但如果要求你查詢詞彙的頻率如同使用者訪問的頻率一樣的話。。。嘿嘿,我相信你一定會大喊“老子不幹了!”。顯然計算機不會這樣喊,它一直是一個勤勤懇懇的員工,不論多麼苛刻的請求他都會完成。所以請透過索引善待你的計算機:D。

在MongoDB中索引的型別與RDBMS中大體一致,我們不做過多重覆,我們來看一下在MongoDB中如何才能更高效的利用索引。

6.1 索引越少越好

索引可以極大地提高查詢效能,那麼索引是不是越多越好?答案是否定的,並且索引並非越多越好,而是越少越好。每當你建立一個索引時,系統會為你新增一個索引表,用於索引指定的列,然而當你對已建立索引的列進行插入或修改時,資料庫則需要對原來的索引表進行重新排序,重新排序的過程非常消耗效能,但應對少量的索引壓力並不是很大,但如果索引的數量較多的話對於效能的影響可想而知。所以在建立索引時需要謹慎建立索引,要把每個索引的功能都要發揮到極致,也就是說在可以滿足索引需求的情況下,索引的數量越少越好

隱式索引

//建立複合索引
db.test.ensureIndex({"age"1,"no"1,"name"1 })

我們在查詢時可以迅速的將age,no欄位進行排序,隱式索引指的是如果我們想要排序的欄位包含在已建立的複合索引中則無需重覆建立索引

db.test.find().sort("age"1,"no"1)

db.test.find().sort("age"1)

如以上兩個排序查詢,均可使用上面的複合索引,而不需要重新建立索引。

翻轉索引

//建立複合索引
db.test.ensureIndex({"age"1})

翻轉索引很好理解,就是我們在排序查詢時無需考慮索引列的方向,例如這個例子中我們在查詢時可以將排序條件寫為”{‘age’: 0}”,依舊不會影響效能。

6.2 索引列顆粒越小越好

什麼叫顆粒越小越好?在索引列中每個資料的重覆數量稱為顆粒,也叫作索引的基數。如果資料的顆粒過大,索引就無法發揮該有的效能。例如,我們擁有一個”age”列索引,如果在”age”列中,20歲佔了50%,如果現在要查詢一個20歲,名叫”Tom”的人,我們則需要在表的50%的資料中查詢,索引的作用大大降低。所以,我們在建立索引時要儘量將資料顆粒小的列放在索引左側,以保證索引發揮最大的作用。                


●編號443,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

 

運維

更多推薦25個技術類微信公眾號

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

    贊(0)

    分享創造快樂