6 Rules of Thumb for MongoDB Schema Design: Part 2javascript
做者 William Zola, Lead Technical Support Engineer at MongoDBjava
(請結合上一篇 MongoDB Shema 設計的6條經驗法則1一塊兒閱讀。)mongodb
關於 在 MongoDB 中爲 1對多
關係建模的旅程中的第二站,上一次我談過了三點基本的 Schema 設計:內嵌,子引用和父引用。還談到了在選擇這些設計時要考慮兩個因素:數組
1對多
中多端
的實體須要獨立存在嗎?網絡
這個關係的基數是什麼樣的:1對少
,1對不少
,仍是1對很是多?less
有了這些基礎技術知識的保障,我能夠繼續討論更多複雜的 Schema 設計, 會涉及 雙向引用和反範式化。ide
若是你想你的 Scheme 設計變得更高級一點,你能夠結合兩個技術而且在你的Schema同時使用着兩種引用,從1端
到多端
的引用和從多端
到1端
的引用。post
好比,讓咱們回到任務追蹤系統。系統中 People
集合保存 Person
文檔, Task
集合保存 Task
文檔,以及一個 一對多
的從Person ->Task
的關係。這個應用須要追蹤全部的一個Person
全部的 Task
。優化
Task
文檔中有一個引用數組, Person
文檔可能看起來像下面這樣:ui
db.person.findOne()
{
_id: ObjectID("AAF1"),
name: "Kate Monster",
tasks [ // array of references to Task documents
ObjectID("ADF9"),
ObjectID("AE02"),
ObjectID("AE73")
// etc
]
}
複製代碼
另外一方面,這個應用的其餘部分要顯示一個 Task 列表 (例如, 顯示在多人項目中的全部任務)而且這個列表還須要快速的查找到一個 Person
應當負責的全部任務。你能夠經過在Task
文檔中添加一個附加的引用來對其進行優化。
db.tasks.findOne()
{
_id: ObjectID("ADF9"),
description: "Write lesson plan",
due_date: ISODate("2014-04-01"),
owner: ObjectID("AAF1") // Reference to Person document
}
複製代碼
這個設計具備 一對不少 Schema 全部的有點和缺點,但還有一些附加優勢。在 Task
文檔中加入一個額外的 owner
引用意味着它能夠很容易地作到快速地找到任務的全部者。但同時也意味着,若是你從新分配任務給另一我的,你須要執行兩條更新。 特別是,你必須同時更新從 Person 到 Task 的應用,以及從 Task 到 Person 的引用。(對於正在閱讀這篇文章的專家來講——沒錯, 使用這個Schema設計 意味着 再也不可能從新將任務分配給一個新的 Person
。 這對咱們的任務追蹤系統是能夠的: 你須要考慮這是否對你的特定方案有效。)
對於零件的例子, 你能夠反範式化零件的名字到 parts[] 數組。做爲參考,這是 Product 文檔反範式化版本。
> 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
]
}
複製代碼
反範式化意味着在顯示產品全部的零件名稱時將沒必要執行應用級別的聯接,但若是你須要關於零件的其餘信息,則必須得執行該聯接。
> db.products.findOne()
{
name : 'left-handed smoke shifter',
manufacturer : 'Acme Corp',
catalog_number: 1234,
parts : [
{ id : ObjectID('AAAA'), name : '#4 grommet' }, // Part name is denormalized
{ id: ObjectID('F17C'), name : 'fan blade assembly' },
{ id: ObjectID('D2AA'), name : 'power switch' },
// etc
]
}
複製代碼
雖然零件名稱的獲取變得容易了,但這也在應用級的聯接中增長了一點客戶端的工做。
// Fetch the product document
> product = db.products.findOne({catalog_number: 1234});
// Create an array of ObjectID()s containing *just* the part numbers
> part_ids = product.parts.map( function(doc) { return doc.id } );
// Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray() ;
複製代碼
反範式化節約了對反範式化數據查找的成本,倒是以一個更高成本的更新爲代價。 若是你在已經將 Part
的Name
反範式化到了Product
文檔中, 那麼在更新 Part
的名稱時,你必須同時更新 Products
集合中每一條出現這個零件的數據。
反範式化只當在讀取與更新比例很高時纔有意義。若是你須要頻繁的讀取這些反範式化的數據,卻不多對它做更新操做,那麼爲了獲得更有效的查詢,一般須要以更新變得緩慢而複雜做爲代價。當更新相對於查詢更爲頻繁時,反範式化所節省下的成本則變少了。
例如,假設零件的名稱常常不常常改動,但現有的零件的數據常常變更。這意味着雖然將 Part
的 Name
範式化到 Product
文檔中有意義,現有零件的數據的反範式化則沒有意義。
同時注意,當你範式化一個字段時,你將沒法對這個字段進行獨立原子更新。就像上面的雙向引用的示例,若是你先在 Part
文檔中更新零件的名字,而後在 Product
文檔中更新零件的名字,將會出現一個不到一秒的時間間隔,這會形成Product
文檔中將不會映射到 Part
文檔中心得更新過的值。
1對不少
的反範式化你也是對自動實現從 1端 到 多端的反範式化:
> db.parts.findOne()
{
_id : ObjectID('AAAA'),
partno : '123-aff-456',
name : '#4 grommet',
product_name : 'left-handed smoke shifter', // Denormalized from the ‘Product’ document
product_catalog_number: 1234, // Ditto
qty: 94,
cost: 0.94,
price: 3.99
}
複製代碼
然而,若是你已經完成了從 Product
名稱 到 Part
文檔的範式化,那麼當你更新 Product
名稱時,你也必須同時更新 Part
集合中每個 Product
出現的地方。這多是一個成本更高的更新,由於你同時更新了的多個 Parts
。 所以,在進行這樣的反範式化時,讀與寫比例的考慮就顯是尤其重要了。
1對很是多
的示例也能夠進行反範式化,經過如下兩種方式:你能夠將 1端
的信息(來自 hots
文檔)放入 很是多
的那一端, 或者你也能夠放入一些很是多端
的總結性的信息到1端
。
下面是反範式化到很是多端
的示例。我將把 host的IP地址 放入到單個的日誌消息中:
> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
ipaddr : '127.66.66.66',
host: ObjectID('AAAB')
}
複製代碼
對於查詢特定IP的最新消息會變得更加容易。如今只須要一次查詢:
> last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()
複製代碼
實際上,若是你只想在 1端
存放必定數量的信息,你能夠將它所有都反範式化到 很是多
那一段,徹底沒必要用到1端
。
> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
ipaddr : '127.66.66.66',
hostname : 'goofy.example.com',
}
複製代碼
另外一方面,你有能夠反範式化到 1端
。假如你想在 host 文檔中保存一個 host
最近1000條信息。你可使用MongoDB 2.4中 介紹的 $each
和 $slice
方法來保存那隻包含了最近1000 條且已排好序的信息。
日誌信息被保存到了 logmsg
集合中,同時也保存到了 host
文檔中的反範式化列表裏。 這樣,即便信息超出了 hosts
。logmsgs
數組,也不會丟失了
// Get log message from monitoring system
logmsg = get_log_msg();
log_message_here = logmsg.msg;
log_ip = logmsg.ipaddr;
// Get current timestamp
now = new Date()
// Find the _id for the host I’m updating
host_doc = db.hosts.findOne({ipaddr : log_ip },{_id:1}); // Don’t return the whole document
host_id = host_doc._id;
// Insert the log message, the parent reference, and the denormalized data into the ‘many’ side
db.logmsg.save({time : now, message : log_message_here, ipaddr : log_ip, host : host_id ) });
// Push the denormalized log message onto the ‘one’ side
db.hosts.update( {_id: host_id },
{$push : {logmsgs : { $each: [ { time : now, message : log_message_here } ],
$sort: { time : 1 }, // Only keep the latest ones
$slice: -1000 } // Only keep the latest 1000
}} );
複製代碼
須要注意,投影規劃可防止MongoDB在網絡中傳輸整個 hosts 文檔。經過告知 MongoDB只須要返回 _id
字段,網絡開銷能夠減小到僅用於村塾該字段的那個幾個字節(還有一些傳輸協議的開銷)。
就像 一對多 案例中同樣,你須要考慮讀取與更新的比例。只有當日志消息相對應用程序須要在全部消息查找單個主機的次數不多時,日誌消息到Host文檔的反範式化才又意義。若是你要查找數據的頻率低於更新的頻率,那這種特殊的反範式化則不是一個好的辦法。
在這篇文章中,我談到了在基本的內嵌,子引用和父引用的其餘選擇。
若是雙向引用能夠優化你的 Schema,而且你能夠接受不能進行原子更新這樣的代價,那麼就可使用雙向引用。
引用時,從 1端 到 N 端
,或者 N端 到 1端
的反範式化都是能夠的,
在以爲是否須要反範式化是,要考慮下面的因素:
你將沒法對反範式化的數據進行原子更新。
在讀較寫的比例高時, 反範式化纔有意義。
下一次,我給大家一些關於這些選項的選擇上的指導。