Mongodb架構設計
參考連結:learnmongodbthehardway.com/schema/sche…
概覽
Mongodb是文件型資料庫,由於其不屬於關係型資料庫,不必遵守三大正規化,而且也沒有Join關鍵字來支援表連線,所以Mongodb的表結構設計跟Oracle、MySQL很不一樣。下面針對幾種不同的表設計結構分別舉例:
1對1關係模型
在關係型資料庫中,1對1關係模型通常是通過外來鍵的形式進行處理。我們以作家跟地址來舉例,假設這兩個實體的關係是1對1,那麼我們可能會像下面這樣子建表
![](https://i.iter01.com/images/f834ac53908ac08589c06d6f4368f4bc0919df3e0c5c7437925f271ec72de0fa.png)
![](https://i.iter01.com/images/a8067b9ae00762bad285537503b9e0a56f7ac1a91e2b286652b22bd277f1fe24.png)
![](https://i.iter01.com/images/3b674b1d73cb6edaa51d879cb5a286b783beebd982138ed8ef3dccc1dcb1c8cd.png)
但是,為了方便,其實我們在設計表的時候不會嚴格遵守三大正規化,會做一定的冗餘資料,實際情況下可能就是這樣子的表
![](https://i.iter01.com/images/cb615cacc8bd90a02b65aa4c41518f71a542e669f2d5f3d5f33668c13ee99916.png)
那麼,我們回到Mongodb,在這張非關係型的NoSQL資料庫裡,沒有標準的外來鍵(雖然我們可以手工建立連線,但是這種表之間的欄位關聯關係只能存在程式級別,資料庫本身並沒有提出外來鍵約束的概念),我們可以怎麼來建立表並處理表之間的關係呢?
建立連線
這種方式可以理解為建立外來鍵,在其中一個表裡建立對方的id欄位
使用者資訊的文件設計 { _id: 1, name: "Peter Wilkinson", age: 27 } 保留外來鍵的地址資訊的文件設計 { user_id: 1, street: "100 some road", city: "Nevermore" }複製程式碼
內嵌文件
直接把地址資訊的文件作為使用者資訊文件的一個欄位儲存進去
{ name: "Peter Wilkinson", age: 27, address: { street: "100 some road", city: "Nevermore" } }複製程式碼
直接內嵌文件的好處就是我們可以在單次讀操作就可以特定使用者的使用者資訊文件以及對應的地址資訊文件,當然了,這是在使用者資訊和地址資訊強關聯的時候,這樣子直接內嵌才顯得有意義。
官方文件推薦1對1的資料模型儘量使用內嵌的方式,這樣子會提高讀操作的效率,更快地獲取文件資訊複製程式碼
1對多關係模型
1對多的關係模型,我們可以簡單地以部落格和對應的評論資訊來舉例
![](https://i.iter01.com/images/e12aedf27aa1a39bf40c1a14e00adb04859ccb18560d74a14b134c9652020ec6.png)
對應的Mongodb的表模型如下
部落格資訊的文件設計
{
title: "An awesome blog",
url: "http://awesomeblog.com",
text: "This is an awesome blog we have just started"
}
評論資訊的文件設計
{
name: "Peter Critic",
created_on: ISODate("2014-01-01T10:01:22Z"),
comment: "Awesome blog post"
}
{
name: "John Page",
created_on: ISODate("2014-01-01T11:01:22Z"),
comment: "Not so awesome blog"
}複製程式碼
在關係型資料庫裡,我們通常是分別建立兩張表:一個Blog表、一個Comments表(從表,帶有blog_id外來鍵),然後通過join操作把兩個表關聯起來
![](https://i.iter01.com/images/106799001093c880a86dcfe6defabda5bb2ac3257852c087aa360ffe5d3174ae.png)
![](https://i.iter01.com/images/924ae3b6fb8c2c8be34b2f90c6a7090d18415495df4aea5136ac3451c0003b08.png)
但是在Mongodb裡由於沒有Join關鍵字,但是我們可以根據Mongodb的特點,得出以下三個解決方式:
內嵌
內嵌了評論資訊的部落格文件設計 { title: "An awesome blog", url: "http://awesomeblog.com", text: "This is an awesome blog we have just started", comments: [{ name: "Peter Critic", created_on: ISODate("2014-01-01T10:01:22Z"), comment: "Awesome blog post" }, { name: "John Page", created_on: ISODate("2014-01-01T11:01:22Z"), comment: "Not so awesome blog" }] }複製程式碼
上面這種表設計的好處是,我們可以直接獲取指定部落格下的評論資訊,使用者新增評論的話,直接在blog文件下的comments陣列欄位插入一個新值即可。
但是這種表設計至少有三個如下的潛在問題需要注意:
- 部落格下的評論陣列可能會逐漸擴增,甚至於超過了文件的最大限制長度:16MB
- 第二個問題是跟寫效能相關,由於評論是不停地新增至部落格文件裡,當有新的部落格文件插入集合的時候,MongoDB會變得比較困難定位到原來的部落格文件位置,另外,資料庫還需要額外開闢新的記憶體空間並複製原來的部落格文件、更新所有索引,這需要更多的IO互動,可能會影響寫效能
![](https://i.iter01.com/images/ef05474191ebe5a634bb965038f6da01a1f4f64e2ab171c08254778486d6db45.png)
必須注意的是,只有高寫入流量的情況下才可能會影響寫效能,而對於寫入流量較小的程式反而沒有那麼大的影響。視具體情況而定。
3. 第三個問題是當你嘗試去進行評論分頁的時候,你會發覺通過常規的find查詢操作,我們只能先讀取整個文件資訊(包括所有評論資訊),然後在程式裡進行評論資訊的分頁複製程式碼
連線
第二個方式是通過建立類似外來鍵的id來進行文件間的關聯
部落格的文件設計 { _id: 1, title: "An awesome blog", url: "http://awesomeblog.com", text: "This is an awesome blog we have just started" } 評論的文件設計 { blog_entry_id: 1, name: "Peter Critic", created_on: ISODate("2014-01-01T10:01:22Z"), comment: "Awesome blog post" } { blog_entry_id: 1, name: "John Page", created_on: ISODate("2014-01-01T11:01:22Z"), comment: "Not so awesome blog" }複製程式碼
這樣子設計模型有個好處是當評論資訊逐漸增長的時候並不會影響原始的部落格文件,從而避免了單個文件超過16MB的情況出現。而且這樣子設計也比較容易返回分頁評論。但是壞處的話,就是假設我們在一個部落格文件下擁有非常多的評論時(比如1000條),那我們獲取所有評論的時候會引起資料庫很多的讀操作複製程式碼
分塊
第三個方法就是前面兩種方法的混合,理論上,嘗試去平衡內嵌策略和連線模式,舉個例子,我們可能會根據實際情況,把所有的評論切分成最多50條評論的分塊
部落格的文件設計 { _id: 1, title: "An awesome blog", url: "http://awesomeblog.com", text: "This is an awesome blog we have just started" } 評論資訊的文件設計 { blog_entry_id: 1, page: 1, count: 50, comments: [{ name: "Peter Critic", created_on: ISODate("2014-01-01T10:01:22Z"), comment: "Awesome blog post" }, ...] } { blog_entry_id: 1, page: 2, count: 1, comments: [{ name: "John Page", created_on: ISODate("2014-01-01T11:01:22Z"), comment: "Not so awesome blog" }] }複製程式碼
這樣子設計最大的好處是我們可以單次讀操作裡一次性抓出50條評論,方便我們進行評論分頁
什麼時候使用分塊策略? 當你可以將文件切割成不同的批次時,那麼採用這種策略可以加速文件檢索 典型的例子就是根據小時、天數或者數量進行評論分頁(類似評論分頁)複製程式碼
多對多關係模型
多對多關係模型,我們以作者跟創作的書籍來舉例
![](https://i.iter01.com/images/66af3308235e3d9b303991fa32e6b70f098a39e8d96052b6f820d59ae1f3ad7e.png)
在關係型資料庫裡,我們可以通過中間表的方式來處理
![](https://i.iter01.com/images/97590b45a795ed72c528daf4404685711ef666071181d2b6bf50eb54e7bcbfdc.png)
![](https://i.iter01.com/images/059ae76cba73f4bd42312ec230cae241642c27d663d8292dc67d7dca7776e9dc.png)
![](https://i.iter01.com/images/274aad0b7528256acd465569d1dba3113460a12c765970e77d9bb4461632d366.png)
雙向巢狀
在MongoDB裡我們可以通過雙向巢狀,把兩個文件的外來鍵通過陣列欄位新增到彼此的文件裡
作者資訊的文件設計 { _id: 1, name: "Peter Standford", books: [1, 2] } { _id: 2, name: "Georg Peterson", books: [2] } 書籍資訊的文件設計 { _id: 1, title: "A tale of two people", categories: ["drama"], authors: [1, 2] } { _id: 2, title: "A tale of two space ships", categories: ["scifi"], authors: [1] }複製程式碼
當我們進行查詢的時候,可以通過兩個維度互相進行查詢
通過指定的作者搜尋對應的書籍
var db = db.getSisterDB("library");
var booksCollection = db.books;
var authorsCollection = db.authors;
var author = authorsCollection.findOne({name: "Peter Standford"});
var books = booksCollection.find({_id: {$in: author.books}}).toArray();
通過指定的書籍搜尋對應的作者
var db = db.getSisterDB("library");
var booksCollection = db.books;
var authorsCollection = db.authors;
var book = booksCollection.findOne({title: "A tale of two space ships"});
var authors = authorsCollection.find({_id: {$in: book.authors}}).toArray();複製程式碼
單向巢狀
單向巢狀策略是用來優化多對多關係模型裡的讀效能,通過將雙向引用轉移為類似一對多的單向引用。這種策略是有特定場景的,比如在我們這個案例中,我們設計的作者資訊文件裡,將書籍資訊作為陣列欄位嵌入作者文件,但是實際情況下,書籍的數量是會快速地增長,很可能會突破單個文件16MB的限制。
在這個案例中,我們可以看到書籍數量是快速增長的,但是書籍分類確實比較固定,通常不會有太大改動,所以我們把書籍分類資訊單獨設計成文件,然後作者資訊作為書籍資訊的嵌入陣列引用,書籍分類也作為嵌入陣列引用。以相對變化不大的書籍分類作為主表,把相對變化較大的書籍資訊作為從表,儲存主表id作為外來鍵。
書籍分類的文件設計 { _id: 1, name: "drama" } 通過外來鍵關聯對應分類的書籍資訊文件設計 { _id: 1, title: "A tale of two people", categories: [1], authors: [1, 2] }複製程式碼
相對應的查詢語句如下
通過指定書籍來查詢對應的書籍分類 var db = db.getSisterDB("library"); var booksCol = db.books; var categoriesCol = db.categories; var book = booksCol.findOne({title: "A tale of two space ships"}); var categories = categoriesCol.find({_id: {$in: book.categories}}).toArray(); 複製程式碼
根據指定書籍分類來查詢對應書籍
var db = db.getSisterDB("library");
var booksCollection = db.books;
var categoriesCollection = db.categories;
var category = categoriesCollection.findOne({name: "drama"});
var books = booksCollection.find({categories: category.id}).toArray();
需要注意的地方:
保持關聯關係的平衡
當多對多關係模型裡,有一個模型數量級別特別大(比如最多可達500000個),另一個數量級別特別小(比如最多3個),像這個案例中,可能才3個左右的書籍分類就可以對應到高達500000本書。在這張數量級別懸殊的情況下,就應該採用這種單向巢狀的策略。如果雙方書籍級別都比較小(可能最多也就是5個左右)的時候,採用雙向巢狀的策略可能會好一點。複製程式碼