MongoDB 傾向於將數據都放在一個 Collection 下嗎?
不是這樣的。
Collection 的單個 doc 有大小上限,現在是 16MB,這就使得你不可能把所有東西都揉到一個 collection 裏。而且如果 collection 結構過於復雜,既會影響查詢、更新效率,也會造成維護困難和操作風險。你有嘗試過手一抖就把一個 doc 不小心存成 null 的麽,反正我做過,要是一個人所有信息都在這個 collection 裏面,那感覺一定相當酸爽吧。
一般的原則是:
- 按照查詢方式來聚類
- 需要經常一起讀取的數據放一起.
- 在邏輯上關系緊密的信息放在一起。
- 有 map-reduce/aggregation 需求的數據放在一起,這些操作都只能操作單個 collection。
- 按照數據量來拆分
- 如果發現要在 collection 裏面用數組,數組長度還會不斷增加,那麽應該把數據內容放到一個專門的 collection,每條數據都引用當前這個 doc 的主鍵(就像 mysql 的 1..N 外鍵依賴一樣)。
- 如果發現某個 doc 層次過深(超過 2 層),八成得考慮要拆分了,要不然性能和可維護性都會有問題。
- 按照有表結構的方式來設計
- MongoDB 是沒有表結構這個概念的,但是實際使用的時候,很少說一個 collection 裏面存在各式各樣結構的 doc,如果發現 doc 的結構差別越來越大了,那麽應該考慮怎麽抽象成類似結構,把變化的東西扔到其他 collection 去,用外鍵依賴的方式互相引用。
比如設計一個用戶系統,user collection 應該放 name 等常用的信息,也應該放 lastLoginAt 這些僅跟 user 相關的東西,或許應該把用戶有哪些訪問權限的信息也放進來,但是不要放用戶的登錄日誌這種信息會不斷增加的信息。
至於 user 之間的關系是否存在 user collection 則需要討論。假如僅僅需要存儲用戶間的關系,記錄下好友的 uid 就行,而且好友數量也不太大,幾百個最多了,那麽我傾向於放在一個 collection 裏。如果關系數據本身就比較復雜,或者好友數會上千,那我傾向於拆分。
另外,Mongodb 官方的 數據模型設計範式 很值得一讀,推薦去好好看看。
- 2014年06月26日回答
- 1 評論
- 贊賞
- 編輯
原文地址: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": "[email protected]",
"following": [
ObjectId("..."),
ObjectId("...")
]
}
現在,對於一個給定的用戶文檔,可以使用形如db.activities.find({"user": {"$in": user["following"]}})
的方式查詢該用戶感興趣的所有活動信息。但是,對於一條剛剛發布的活動信息,如果要找出對這條信息感興趣的所有用戶,就不得不查詢所有用戶的“following”字段了。
另一種方式是將訂閱者內嵌到生產者文檔中:
{
"_id": ObjectId("..."),
"username": "joker",
"email": "[email protected]",
"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": "[email protected]",
"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.
- 2014年06月26日回答
- 評論
- 贊賞
- 編輯
如果業務總是需要查詢用戶間關系 不如還是把關系獨立一個Collection
https://segmentfault.com/q/1010000000589390
MongoDB 傾向於將數據都放在一個 Collection 下嗎?