摘要:反范式化與范式化相反將每個文檔所需的數據都嵌入在文檔內部。決定何時采用范式化何時采用反范式化時比較困難的。范式化能夠提高數據寫入速度,反范式化能夠提高數據讀取速度。
原文地址:http://pwhack.me/post/2014-06-25-1 轉載注明出處
本文摘錄自《MongoDB權威指南》第八章,可以徹底回答以下兩個問題:
http://segmentfault.com/q/1010000000364944
http://segmentfault.com/q/1010000000364944
數據表示的方式有很多種,其中最重要的問題之一就是在多大程度上對數據進行范式化。范式化(normalization)是將數據分散到多個不同的集合,不同集合之間可以相互引用數據。雖然很多文檔可以引用某一塊數據,但是這塊數據只存儲在一個集合中。所以,如果要修改這塊數據,只需修改保存這塊數據的那一個文檔就行了。但是,MongoDB沒有提供連接(join)工具,所以在不同集合之間執行連接查詢需要進行多次查詢。
反范式化(denormalization)與范式化相反:將每個文檔所需的數據都嵌入在文檔內部。每個文檔都擁有自己的數據副本,而不是所有文檔共同引用同一個數據副本。這意味著,如果信息發生了變化,那么所有相關文檔都需要進行更新,但是在執行查詢時,只需要一次查詢,就可以得到所有數據。
決定何時采用范式化何時采用反范式化時比較困難的。范式化能夠提高數據寫入速度,反范式化能夠提高數據讀取速度。需要根據自己應用程序的十幾需要仔細權衡。
數據表示的例子假設要保存學生和課程信息。一種表示方式是使用一個students集合(每個學生是一個文檔)和一個classes集合(每門課程是一個文檔)。然后用第三個集合studentsClasses保存學生和課程之間的聯系。
> db.studentsClasses.findOne({"studentsId": id}); { "_id": ObjectId("..."), "studentId": ObjectId("..."); "classes": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
如果比較熟悉關系型數據庫,可能你之前建國這種類型的表連接,雖然你的每個記過文檔中可能只有一個學生和一門課程(而不是一個課程“_id”列表)。將課程放在數組中,這有點兒MongoDB的風格,不過實際上通常不會這么保存數據,因為要經歷很多次查詢才能得到真實信息。
假設要找到一個學生所選的課程。需要先查找students集合找到學生信息,然后查詢studentClasses找到課程“_id”,最后再查詢classes集合才能得到想要的信息。為了找出課程信息,需要向服務器請求三次查詢。很可能你并不想再MongoDB中用這種數據組織方式,除非學生信息和課程信息經常發生變化,而且對數據讀取速度也沒有要求。
如果將課程引用嵌入在學生文檔中,就可以節省一次查詢:
{ "_id": ObjectId("..."), "name": "John Doe", "classes": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
"classes"字段是一個數組,其中保存了John Doe需要上的課程“_id”。需要找出這些課程的信息時,就可以使用這些“_id”查詢classes集合。這個過程只需要兩次查詢。如果數據不需要隨時訪問也不會隨時發生變化(“隨時”比“經?!币蟾撸?,那么這種數據組織方式是非常好的。
如果需要進一步優化讀取速度,可以將數據完全反范式化,將課程信息作為內嵌文檔保存到學生文檔的“classes”字段中,這樣只需要一次查詢就可以得到學生的課程信息了:
{ "_id": ObjectId("..."), "name": "John Doe" "classes": [ { "class": "Trigonometry", "credites": 3, "room": "204" }, { "class": "Physics", "credites": 3, "room": "159" }, { "class": "Women in Literature", "credites": 3, "room": "14b" }, { "class": "AP European History", "credites": 4, "room": "321" } ] }
上面這種方式的優點是只需要一次查詢就可以得到學生的課程信息,缺點是會占用更多的存儲空間,而且數據同步更困難。例如,如果物理學的學分變成了4分(不再是3分),那么選修了物理學課程的每個學生文檔都需要更新,而且不只是更新“Physics”文檔。
最后,也可以混合使用內嵌數據和引用數據:創建一個子文檔數組用于保存常用信息,需要查詢更詳細信息時通過引用找到實際的文檔:
{ "_id": ObjectId("..."), "name": "John Doe", "classes": [ { "_id": ObjectId("..."), "class": "Trigonometry" }, { "_id": ObjectId("..."), "class": "Physics" }, { "_id": ObjectId("..."), "class": "Women in Literature" }, { "_id": ObjectId("..."), "class": "AP European History" } ] }
這種方式也是不錯的選擇,因為內嵌的信息可以隨著需求的變化進行修改,如果希望在一個頁面中包含更多(或者更少)的信息,就可以將更多(或者更少)的信息放在內嵌文檔中。
需要考慮的另一個重要問題是,信息更新更頻繁還是信息讀取更頻繁?如果這些數據會定期更新,那么范式化是比較好的選擇。如果數據變化不頻繁,為了優化更新效率兒犧牲讀寫速度就不值得了。
例如,教科書上介紹范式化的一個例子可能是將用戶和用戶地址保存在不同的集合中。但是,人們幾乎不會改變住址,所以不應該為了這種概率極小的情況(某人改變了住址)而犧牲每一次查詢的效率。在這種情況下,應該將地址內嵌在用戶文檔中。
如果決定使用內嵌文檔,更新文檔時,需要設置一個定時任務(cron job),以確保所做的每次更新都成功更新了所有文檔。例如,我們試圖將更新擴散到多個文檔,在更新完成所有文檔之前,服務器崩潰了。需要能夠檢測到這種問題,并且重新進行未完的更新。
一般來說,數據生成越頻繁,就越不應該將這些內嵌到其他文檔中。如果內嵌字段或者內嵌字段數量時無限增長的,那么應該將這些內容保存在多帶帶的集合中,使用引用的方式進行訪問,而不是內嵌到其他文檔中,評論列表或者活動列表等信息應該保存在多帶帶的集合中,不應該內嵌到其他文檔中。
最后,如果某些字段是文檔數據的一部分,那么需要將這些字段內嵌到文檔中。如果在查詢文檔時經常需要將某個字段排除,那么這個字段應該放在另外的集合中,而不是內嵌在當前的文檔中。
更適合內嵌 | 更適合引用 |
---|---|
子文檔較小 | 子文檔較大 |
數據不會定期改變 | 數據經常改變 |
最終數據一致即可 | 中間階段的數據必須一致 |
文檔數據小幅增加 | 文檔數據大幅增加 |
數據通常需要執行二次查詢才能獲得 | 數據通常不包含在結果中 |
快速讀取 | 快速寫入 |
假如我們有一個用戶集合。下面是一些可能需要的字段,以及它們是否應該內嵌到用戶文檔中。
用戶首選項(account preferences)用戶首選項只與特定用戶相關,而且很可能需要與用戶文檔內的其他用戶信息一起查詢。所以用戶首選項應該內嵌到用戶文檔中。
最近活動(recent activity)這個字段取決于最近活動增長和變化的頻繁程度。如果這是個固定長度的字段(比如最近的10次活動),那么應該將這個字段內嵌到用戶文檔中。
好友(friends)通常不應該將好友信息內嵌到用戶文檔中,至少不應該將好友信息完全內嵌到用戶文檔中。下節會介紹社交網絡應用的相關內容。
所有由用戶產生的內容不應該內嵌在用戶文檔中。
基數一個集合中包含的對其他集合的引用數量叫做基數(cardinality)。常見的關系有一對一、一對多、多對多。假如有一個博客應用程序。每篇博客文章(post)都有一個標題(title),這是一個對一個的關系。每個作者(author)可以有多篇文章,這是一個對多的關系。每篇文章可以有多個標簽(tag),每個標簽可以在多篇文章中使用,所以這是一個多對多的關系。
在MongoDB中,many(多)可以被分拆為兩個子分類:many(多)和few(少)。假如,作者和文章之間可能是一對少的關系:每個作者只發表了為數不多的幾篇文章。博客文章和標簽可能是多對少的關系:文章數量實際上很可能比標簽數量多。博客文章和評論之間是一對多的關系:每篇文章可以擁有很多條評論。
只要確定了少與多的關系,就可以比較容易地在內嵌數據和引用數據之間進行權衡。通常來說,“少”的關系使用內嵌的方式會比較好,“多”的關系使用引用的方式比較好。
好友、粉絲、以及其他的麻煩事情親近朋友,遠離敵人
很多社交類的應用程序都需要鏈接人、內容、粉絲、好友,以及其他一些事物。對于這些高度關聯的數據使用內嵌的形式還是引用的形式不容易權衡。這一節會介紹社交圖譜數據相關的注意事項。通常,關注、好友或者收藏可以簡化為一個發布、訂閱系統:一個用戶可以訂閱另一個用戶相關的通知。這樣,有兩個基本操作需要比較高效:如何保存訂閱者,如何將一個事件通知給所有訂閱者。
比較常見的訂閱實現方式有三種。第一種方式是將內容生產者內嵌在訂閱者文檔中:
{ "_id": ObjectId("..."), "username": "batman", "email": "batman@waynetech.com", "following": [ ObjectId("..."), ObjectId("...") ] }
現在,對于一個給定的用戶文檔,可以使用形如db.activities.find({"user": {"$in": user["following"]}})的方式查詢該用戶感興趣的所有活動信息。但是,對于一條剛剛發布的活動信息,如果要找出對這條信息感興趣的所有用戶,就不得不查詢所有用戶的“following”字段了。
另一種方式是將訂閱者內嵌到生產者文檔中:
{ "_id": ObjectId("..."), "username": "joker", "email": "joker@mailinator.com", "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
當這個生產者新發布一條信息時,我們立即就可以知道需要給哪些用戶發布通知。這樣做的缺點時,如果需要找到一個用戶關注的用戶列表,就必須查詢整個用戶集合。這樣方式的優缺點與第一種方式的優缺點恰好相反。
同時,這兩種方式都存在另一個問題:它們會使用戶文檔變得越來越大,改變也越來越頻繁。通常,“following”和“followers”字段甚至不需要返回:查詢粉絲列表有多頻繁?如果用戶比較頻繁地關注某些人或者對一些人取消關注,也會導致大量的碎片。因此,最后的方案對數據進一步范式化,將訂閱信息保存在多帶帶的集合中,以避免這些缺點。進行這種成都的范式化可能有點兒過了,但是對于經常發生變化而且不需要與文檔其他字段一起返回的字段,這非常有用。對“followers”字段做這種范式化使有意義的。
用一個集合來保存發布者和訂閱者的關系,其中的文檔結構可能如下所示:
{ "_id": ObjectId("..."), //被關注者的"_id" "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
這樣可以使用戶文檔比較精簡,但是需要額外的查詢才能得到粉絲列表。由于“followers”數組的大小經常會發生變化,所以可以在這個集合上啟用“usePowerOf2Sizes”,以保證users集合盡可能小。如果將followers集合保存在另一個數據庫中,也可以在不過多影響users集合的前提下對其進行壓縮。
應對威爾惠頓效應不管使用什么樣的策略,內嵌字段只能在子文檔或者引用數量不是特別大的情況下有效發揮作用。對于比較有名的用戶,可能會導致用于保存粉絲列表的文檔溢出。對于這種情況的一種解決方案使在必要時使用“連續的”文檔。例如:
> db.users.find({"username": "wil"}) { "_id": ObjectId("..."), "username": "wil", "email": "wil@example.com", "tbc": [ ObjectId("123"), // just for example ObjectId("456") // same as above ], "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] } { "_id": ObjectId("123"), "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] } { "_id": ObjectId("456"), "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] }
對于這種情況,需要在應用程序中添加從“tbc”(to be continued)數組中取數據的相關邏輯。
說點什么No silver bullet.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/18705.html
閱讀 1402·2021-10-11 10:59
閱讀 3103·2019-08-30 15:54
閱讀 2723·2019-08-30 13:19
閱讀 2455·2019-08-30 13:02
閱讀 2371·2019-08-30 10:57
閱讀 3346·2019-08-29 15:40
閱讀 980·2019-08-29 15:39
閱讀 2299·2019-08-29 12:40