一.范式化與反范式化
范式的優點:
1)范式化的數據庫更新起來更加快;2)范式化之后,只有很少的重復數據,只需要修改更少的數據;3)范式化的表更小,可以在內存中執行;4)很少的冗余數據,在查詢的時候需要更少的distinct或者group by語句。
范式的缺點:
1)范式化的表,在查詢的時候經常需要很多的關聯,因為單獨一個表內不存在冗余和重復數據。這導致,稍微復雜一些的查詢語句在查詢范式的schema上都可能需要較多次的關聯。這會增加讓查詢的代價,也可能使一些索引策略無效。因為范式化將列存放在不同的表中,而這些列在一個表中本可以屬于同一個索引。
反范式的優點:
1)可以避免關聯,因為所有的數據幾乎都可以在一張表上顯示;2)可以設計有效的索引;
反范式的缺點:
1)表格內的冗余較多,刪除數據時候會造成表有些有用的信息丟失。
在項目設計階段,明確集合的用途是對性能調優非常重要的一步。
從性能優化的角度來看,集合的設計我們需要考慮的是集合中數據的常用操作,例如我們需要設計一個日志(log)集合,日志的查看頻率不高,但寫入頻率卻很高,那么我們就可以得到這個集合中常用的操作是更新(增刪改)。如果我們要保存的是城市列表呢?顯而易見,這個集合是一個查看頻率很高,但寫入頻率很低的集合,那么常用的操作就是查詢。
對于頻繁更新和頻繁查詢的集合,我們最需要關注的重點是他們的范式化程度,在上篇范式化與反范式化的介紹中我們了解到,范式化與反范式化的合理運用對于性能的提高至關重要。然而這種設計的使用非常靈活,假設現在我們需要存儲一篇圖書及其作者,在MongoDB中的關聯就可以體現為以下幾種形式:
1.1完全分離(范式化設計)
示例1:
{ "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "Java小筆記", "author" : [ ObjectId("144b5d83041c7dca84416"), ObjectId("144b5d83041c7dca84418"), ObjectId("144b5d83041c7dca84420"), ] }
我們將作者(comment) 的id數組作為一個字段添加到了圖書中去。這樣的設計方式是在非關系型數據庫中常用的,也就是我們所說的范式化設計。在MongoDB中我們將與主鍵沒有直接關系的圖書單獨提取到另一個集合,用存儲主鍵的方式進行關聯查詢。當我們要查詢文章和評論時需要先查詢到所需的文章,再從文章中獲取評論id,最后用獲得的完整的文章及其評論。在這種情況下查詢性能顯然是不理想的。但當某位作者的信息需要修改時,范式化的維護優勢就凸顯出來了,我們無需考慮此作者關聯的圖書,直接進行修改此作者的字段即可。
1.2.完全內嵌(反范式化設計)
示例2:
{ "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "Java小筆記", "author" : [ { "name" : "微子" "age" : 20, "nationality" : "china", }, { "name" : "星子" "age" : 18, "nationality" : "china", }, { "name" : "原子" "age" : 19, "nationality" : "china", }, ] }
在這個示例中我們將作者的字段完全嵌入到了圖書中去,在查詢的時候直接查詢圖書即可獲得所對應作者的全部信息,但因一個作者可能有多本著作,當修改某位作者的信息時時,我們需要遍歷所有圖書以找到該作者,將其修改。
1.3.部分內嵌(折中方案)
示例3:
{ "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "如何使用MongoDB", "author" : [ { "_id" : ObjectId("144b5d83041c7dca84416"), "name" : "微子" }, { "_id" : ObjectId("144b5d83041c7dca84418"), "name" : "星子" }, { "_id" : ObjectId("144b5d83041c7dca84420"), "name" : "原子" }, ] }
這次我們將作者字段中的最常用的一部分提取出來。當我們只需要獲得圖書和作者名時,無需再次進入作者集合進行查詢,僅在圖書集合查詢即可獲得。
這種方式是一種相對折中的方式,既保證了查詢效率,也保證的更新效率。但這樣的方式顯然要比前兩種較難以掌握,難點在于需要與實際業務進行結合來尋找合適的提取字段。如同示例3所述,名字顯然不是一個經常修改的字段,這樣的字段如果提取出來是沒問題的,但如果提取出來的字段是一個經常修改的字段(比如age)的話,我們依舊在更新這個字段時需要大范圍的尋找并依此進行更新。
在上面三個示例中,第一個示例的更新效率是最高的,但查詢效率是最低的,而第二個示例的查詢效率最高,但更新效率最低。所以在實際的工作中我們需要根據自己實際的需要來設計表中的字段,以獲得最高的效率。
二.填充因子
何為填充因子?填充因子(padding factor)是MongoDB為文檔的擴展而預留的增長空間,因為MongoDB的文檔是以順序表的方式存儲的,每個文檔之間會非常緊湊,如圖所示。
1.元素之間沒有多余的可增長空間。
2.當我們對順序表中某個元素的大小進行增長的時候,就會導致原來分配的空間不足,只能要求其向后移動。
3.當修改元素移動后,后續插入的文檔都會提供一定的填充因子,以便于文檔頻繁的修改,如果沒有不再有文檔因增大而移動的話,后續插入的文檔的填充因子會依此減小。
填充因子的理解之所以重要,是因為文檔的移動非常消耗性能,頻繁的移動會大大增加系統的負擔,在實際開發中最有可能會讓文檔體積變大的因素是數組,所以如果我們的文檔會頻繁修改并增大空間的話,則一定要充分考慮填充因子。
那么如果我們的文檔是個常常會擴展的話,應該如何提高性能?
兩種方案
2.1.增加初始分配空間。在集合的屬性中包含一個 usePowerOf2Sizes 屬性,當這個選項為true時,系統會將后續插入的文檔,初始空間都分配為2的冪數。
這種分配機制適用于一個數據會頻繁變更的集合使用,他會給每個文檔留有更大的空間,但因此空間的分配不會像原來那樣高效,如果你的集合在更新時不會頻繁的出現移動現象,這種分配方式會導致寫入速度相對變慢。
2.2.我們可以利用數據強行將初始分配空間擴大。
db.book.insert({ "name" : "MongoDB", "publishing" : "清華大學出版社", "author" : "john" "tags" : [] "stuff" : "---------- ---------- ----------" })
這樣看起來可能不太優雅,但有時卻很有效!當我們對這個文檔進行增長式修改時,只要將stuff字段刪掉即可。當然,這個stuff字段隨便你怎么起名,包括里邊的填充字符當然也是可以隨意添加的。
三.準確利用索引
索引對于一個數據庫的影響相信大家一定了解,如果一個查詢命令進入到數據庫中后,查詢優化器沒有找到合適的索引,那么數據庫會進行全集合掃描(在RDBMS中也叫全表掃描),全集合查詢對于性能的影響是災難性的。沒有索引的查詢就如同在詞典那毫無規律的海量詞匯中獲得某個你想要的詞匯,但這個詞典是沒有目錄的,只能通過逐頁來查找。這樣的查找可能會讓你耗費幾個小時的時間,但如果要求你查詢詞匯的頻率如同用戶訪問的頻率一樣的話。。。嘿嘿,我相信你一定會大喊“老子不干了!”。顯然計算機不會這樣喊,它一直是一個勤勤懇懇的員工,不論多么苛刻的請求他都會完成。所以請通過索引善待你的計算機。但使用索引有兩點需要注意:1. 索引越少越好;2. 索引顆粒越少越好。
在MongoDB中索引的類型與RDBMS中大體一致,我們不做過多重復,我們來看一下在MongoDB中如何才能更高效的利用索引。
3.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}",依舊不會影響性能。
3.2.索引列顆粒越小越好
什么叫顆粒越小越好?在索引列中每個數據的重復數量稱為顆粒,也叫作索引的基數。如果數據的顆粒過大,索引就無法發揮該有的性能。例如,我們擁有一個"age"列索引,如果在"age"列中,20歲占了50%,如果現在要查詢一個20歲,名叫"Tom"的人,我們則需要在表的50%的數據中查詢,索引的作用大大降低。所以,我們在建立索引時要盡量將數據顆粒小的列放在索引左側,以保證索引發揮最大的作用。
四.存儲引擎優化
MongoDB只有一個存儲引擎,叫做MMAP,MongoDB3.0的推出使得MongoDB有了兩個引擎:MMAPv1和WiredTiger。
MMAPv1:適應于所有MongoDB版本,MongoDB3.0的默認引擎WiredTiger:僅支持64位MongoDB
MMAPv1引擎
MMAPv1預分配策略
MongoDB為了保證連續的存儲空間,避免磁盤碎片問題會預分配空間。 工作方式是這樣的:在創建數據庫時,系統會創建一個名為[dbName].0的文件,該文件固定大小為64M,當該文件有一半以上被使用時,系統會再創建一個名為[dbName].1的文件,該文件大小是方才的兩倍。以此類推,接下來創建的[db Name].n都是[dbName].n-1的兩倍,最大直到2048M,此后,再次創建的文件大小都為2048M。因此如果數據足夠多,64M, 128M, 256M, 1024M, 2048M, 2048M…大小的文件會被創建。下圖為數據庫中的數據文件。
MongoDB為了保證連續的存儲空間,避免磁盤碎片問題會預分配空間。
工作方式是這樣的:在創建數據庫時,系統會創建一個名為[dbName].0的文件,該文件固定大小為64M,當該文件有一半以上被使用時,系統會再創建一個名為[dbName].1的文件,該文件大小是方才的兩倍。以此類推,接下來創建的[db Name].n都是[dbName].n-1的兩倍,最大直到2048M,此后,再次創建的文件大小都為2048M。因此如果數據足夠多,64M, 128M, 256M, 1024M, 2048M, 2048M…大小的文件會被創建。下圖為數據庫中的數據文件。
空間釋放
MongoDB自己不會釋放空間,需要根據實際情況考慮策略。我們刪除MongoDB中的數據后,MongoDB不會釋放空間,在此基礎上再次插入數據后,數據將占用刪除后的空間,即不再需要重新開辟空間。 我們可以采用repair或compact命令主動回收,compact命令是對于某個collection(表),而repair是針對一個數據庫。repaire命令執行時會停止數據庫讀寫操作。(目前使用repair命令可實現空間釋放,但是compact命令執行之后沒有效果,需要再研究)。
WiredTiger引擎
文檔級鎖(Document Level Locking)
WiredTiger增加了文檔級鎖的概念,想比于MMAP的集合級鎖,文檔級鎖可以讓多個客戶端同時修改同一個集合中的不同數據。
壓縮(Compression)
使用WiredTiger引擎,MongoDB可以壓縮所有的集合和索引,相對于MMAPv1,MongoDB可以壓縮最大80%的空間。
歡迎關注微信公眾號:Java小筆記(ijavanote)