举个例子,有一个用户信息和用户间关系的数据库,如果按照 SQL 的思路,会建立用户信息和用户关系两张表。那么,在 MongoDB 中,是倾向于将用户关系嵌入到用户信号,组成一个单独的文档吗?
天蓬老师2017-04-24 09:11:33
不是這樣的。
Collection 的單一 doc 有大小上限,現在是 16MB,這使得你不可能把所有東西都揉到一個 collection 裡。而且如果 collection 結構過於複雜,既會影響查詢、更新效率,也會造成維修困難和操作風險。你有嘗試過手一抖就把一個 doc 不小心存成 null 的麼,反正我做過,要是一個人所有信息都在這個 collection 裡面,那感覺一定相當酸爽吧。
一般的原則是:
例如設計一個使用者係統,user collection 應該放name 等常用的信息,也應該放lastLoginAt 這些僅跟user 相關的東西,或許應該把用戶有哪些訪問權限的信息也放進來,但是不要放用戶的登錄日誌這種資訊會不斷增加的資訊。
至於 user 之間的關係是否存在 user collection 則需要討論。如果只需要儲存用戶間的關係,記錄下好友的 uid 就行,而且好友數量也不太大,幾百個最多了,那麼我傾向於放在一個 collection 裡。如果關係資料本身比較複雜,或是好友數會上千,那我傾向拆分。
另外,Mongodb 官方的 資料模型設計範式 很值得一讀,推薦好好看看。
怪我咯2017-04-24 09:11:33
原文網址:http://pwhack.me/post/2014-06-25-1 轉載註明出處
本文摘錄自《MongoDB權威指南》第八章,可以徹底回答以下兩個問題:
資料表示的方式有很多種,其中最重要的問題之一就是在多大程度上對資料進行範式化。範式化(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),以確保所做的每次更新都成功更新了所有文檔。例如,我們試圖將更新擴散到多個文檔,在更新完成所有文檔之前,伺服器崩潰了。需要能夠偵測到這種問題,並且重新進行未完成的更新。
一般來說,資料產生越頻繁,就越不應該將這些內嵌到其他文件中。如果內嵌欄位或內嵌欄位數量時無限增長的,那麼應該將這些內容保存在單獨的集合中,使用引用的方式進行訪問,而不是內嵌到其他文件中,評論清單或活動清單等資訊應該保存在單獨的集合中,不應該內嵌到其他文件中。
最後,如果某些欄位是文件資料的一部分,那麼需要將這些欄位內嵌到文件中。如果在查詢文件時經常需要將某個欄位排除,那麼這個欄位應該放在另外的集合中,而不是內嵌在目前的文件中。
更適合內嵌 | 比較適合引用 |
---|---|
子文檔較小 | 子文檔較大 |
資料不會定期改變 | 資料經常改變 |
最終數據一致即可 | 中間階段的數據必須一致 |
文檔資料小幅增加 | 文檔資料大幅增加 |
資料通常需要執行二次查詢才能取得 | 資料通常不包含在結果中 |
快速讀取 | 快速寫入 |
假如我們有一個使用者集合。以下是一些可能需要的字段,以及它們是否應該內嵌到使用者文件中。
用戶首選項只與特定用戶相關,而且很可能需要與用戶文件內的其他用戶資訊一起查詢。所以使用者首選項應該內嵌到使用者文件中。
這個欄位取決於最近活動成長和變化的頻繁程度。如果這是一個固定長度的欄位(例如最近的10次活動),那麼應該將這個欄位內嵌到使用者文件中。
通常不應該將好友資訊內嵌到使用者文件中,至少不應該將好友資訊完全內嵌到使用者文件中。下節會介紹社群網路應用的相關內容。
不應該內嵌在使用者文件中。
一個集合中包含的對其他集合的引用數量叫做基數(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.