【譯】MongoDB Shema 設計的6條經驗法則 1
6 Rules of Thumb for MongoDB Schema Design: Part 1
作者 William Zola, Lead Technical Support Engineer at MongoDB
“我有很多使用SQL的經驗, 但在對於 MongoDB 還只是一個初學者。我應該如何為 一對多
的關係建模呢?” 這是我工作時從 MongoDB 使用者收到很常見問題。
對於這個問題我沒辦法給出一個簡短的答案,因為方法有很多,不止某一種。MongoDB 有一個豐富而細微的語彙去表達在SQL中被扁平化為術語 “1 to N ” 的內容。
這個話題有很多可以講的,我將其分成三個部分。
第一個部分,我會談三個基礎的方法去建立 1對多
關係模型。
第二個話題,會涉及複雜的 Shema 設計,包括反正規化化和雙向引用。
最後一部分,我會回顧各種各樣的選擇,並提供一些在考慮構建一個 1對多
關係模型時的建議。
許多初學者認為 MongoDB 中構建 1對多
關係模型的唯一方法是 在父檔案中嵌入一個子檔案陣列,但這個是不對的。你可以嵌入一個檔案,但不意味著你應該嵌入一個檔案。
在設計 MongoDB Shema 時, 你需要從一個你在使用 SQL 時從來沒想過的問題開始:這個關係的 基數 (cardinality,指 N 端的資料規模 ) 是怎麼樣的?簡言之:你需要更加細微地表述你的 1對多
1對少
,1對很多
,還是 1 對非常多
?根據它是哪種,你可以去選擇不同的方式對關係建模。
基礎知識:1對少 建模
一個人的地址可以作為 1對少 的示例。這個是一個關於內嵌子檔案的好案例 ——在 Person
物件中嵌入一組 address
檔案:
> db.person.findOne()
{
name: 'Kate Monster',ssn: '123-456-7890',addresses : [
{ street: '123 Sesame St',city: 'Anytown',cc: 'USA' },{ street: '123 Avenue Q',city : 'New York',cc: 'USA' }
]
}
複製程式碼
這種設計具有內嵌子檔案的所有優缺點。主要的優點是 不必執行一個單獨查詢來獲取內嵌的資料,主要的缺點則是內嵌的資料沒辦法作為一個單獨的實體進行訪問。
例如,你在為一個任務追蹤系統建模,每一個 Person
都會有很多工分配到他們。在Person
檔案中的內嵌任務會讓 如顯示由於明天到期的任務 這樣的查詢變得比其實際需要的更加困難。對於這種案例,我會在下一個部分提供一個更合適的設計。
基礎知識:1對很多 建模
替換零部件訂購系統中產品的零部件可以作為 1對多
的示例。每一個產品可能都有幾百個替換李建,但絕不會超過幾千個左右(所有這些不同尺寸的螺栓,墊圈和墊圈加起來)。這是一個很好的參考案例,你可以將零件的 ObjectID
內嵌到產品的問題中。(這個示例中,我使用2位元組的 ObjectID
,因為它們更易於閱讀:實際程式碼將使用12位元組的 ObjectID
)
每一個零部件 _( parts )_會有它們的自己的檔案:
> db.parts.findOne()
{
_id : ObjectID('AAAA'),partno : '123-aff-456',name : '#4 grommet',qty: 94,cost: 0.94,price: 3.99
}
複製程式碼
每一個產品 _( products )_也會有它們自己的檔案,其檔案中還會所以包含組成產品的 parts
的 ObjectID
陣列:
> db.products.findOne()
{
name : 'left-handed smoke shifter',manufacturer : 'Acme Corp',catalog_number: 1234,parts : [ // array of references to Part documents
ObjectID('AAAA'),// reference to the #4 grommet above
ObjectID('F17C'),// reference to a different Part
ObjectID('D2AA'),// etc
]
}
複製程式碼
然後,您再使用應用程式級聯接來檢索特定產品的零件:
// Fetch the Product document identified by this catalog number
> product = db.products.findOne({catalog_number: 1234});
// Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;
複製程式碼
為了操作更加高效,你可能還需要為products.catalog_number
建立索引。注意parts._id
一定有索引的,因此查詢 parts
會很快。
這種風格的引用能與內嵌檔案的優缺點起到互補的左右。每一個零件都是一個單獨的檔案,因此很容易對他們做獨立的查詢和更新。使用這個 Schema 也需要一點妥協,就是在獲取查詢零部件詳細資訊時需要在再做一次查詢。(但先保留這個問題,直到我們進入 part 2 - 反正規化化)。
作為一個額外的好處,這種 Schema 允許你在多個產品中使用不同的零件,所以你的 1對多
Schema 就變成了多對多
的 Schema 不需要任何關聯表了。
基礎知識:1對非常多 建模
從不同機器上收集日誌資訊的事件日誌系統可以作為 1對很多
的示例。任何給定 host 可以生產超過16Mb 的檔案大小,即使你的的陣列中村咋都是 ObjectID
。這是一個 “父引用”經典案例 — 你可有一個host 檔案,並且在每一個日誌資訊檔案中儲存這個 Host 的 ObjectID 。
> db.hosts.findOne()
{
_id : ObjectID('AAAB'),name : 'goofy.example.com',ipaddr : '127.66.66.66'
}
>db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),message : 'cpu is on fire!',host: ObjectID('AAAB') // Reference to the Host document
}
複製程式碼
你可以用一個有一點點不同的應用級別的聯合查詢得到一個host最近5000條Messages:
// find the parent ‘host’ document
> host = db.hosts.findOne({ipaddr : '127.66.66.66'}); // assumes unique index
// find the most recent 5000 log message documents linked to that host
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()
複製程式碼
回顧
如此可見,即使在這個基礎的級別,設計 MongoDB Schema 時 比 設計一個對比的 關係性的 Schema要思考的更多。有以下兩個的因素需要考慮:
-
1對多 中"多"端的實體需要獨立存在嗎?
-
這個關係的基數是什麼樣的:1對少,1對很多,還是1對非常多?
基於這些因素,你可以選擇三個基本的 1對多 Schema 設計:
-
如果基數是 1對少並且不需要從父物件的上下文之外訪問內嵌的物件,則直接內嵌N端;
-
如果基數為一對多 或者 N 端物件因故需要獨立存在,則內嵌 N 端物件的引用陣列。
-
如果基數為一對很多,則在 N 端物件中引用 1端物件。
下一次,我們將瞭解如何使用雙向關係和非規範化來增強這些基礎的 Schema 效能。
寫在後面:
最開始是在這篇文章 MongoDB資料庫設計中6條重要的經驗法則 裡看到的,但是因為排版不是很容易閱讀,因此我直接閱讀了原文,並自己試著從新翻譯排版, 部分術語的理解也參考了 MongoDB資料庫設計中6條重要的經驗法則 。
文章雖然已經比較久了,但對我才開始使用的 MongoDB 的初學者來講,裡面的知識點還是非常有價值的。後面還有兩篇,會接著看完並翻譯發表。希望對看到的人也有所幫助。