1. 程式人生 > 程式設計 >【譯】MongoDB Shema 設計的6條經驗法則 2

【譯】MongoDB Shema 設計的6條經驗法則 2

6 Rules of Thumb for MongoDB Schema Design: Part 2

作者 William Zola, Lead Technical Support Engineer at MongoDB


(請結合上一篇 MongoDB Shema 設計的6條經驗法則1一起閱讀。)


關於 在 MongoDB 中為 1對多 關係建模的旅程中的第二站,上一次我談過了三點基本的 Schema 設計:內嵌,子引用和父引用。還談到了在選擇這些設計時要考慮兩個因素:

  1. 1對多多端的實體需要獨立存在嗎?

  2. 這個關係的基數是什麼樣的:1對少1對很多,還是1對非常多?

有了這些基礎技術知識的保障,我可以繼續討論更多複雜的 Schema 設計, 會涉及 雙向引用和反正規化化。

中級:雙向引用

如果你想你的 Scheme 設計變得更高階一點,你可以結合兩個技術並且在你的Schema同時使用著兩種引用,從1端多端的引用和從多端1端的引用。

比如,讓我們回到任務追蹤系統。系統中 People 集合儲存 Person 檔案, Task 集合儲存 Task 檔案,以及一個 一對多 的從Person ->Task 的關係。這個應用需要追蹤所有的一個Person所有的 Task

Task 檔案中有一個引用陣列, Person 檔案可能看起來像下面這樣:

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',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() ;
複製程式碼

反正規化化節約了對反正規化化資料查詢的成本,卻是以一個更高成本的更新為代價。 如果你在已經將 PartName 反正規化化到了Product 檔案中, 那麼在更新 Part 的名稱時,你必須同時更新 Products 集合中每一條出現這個零件的資料。

反正規化化只當在讀取與更新比例很高時才有意義。如果你需要頻繁的讀取這些反正規化化的資料,卻很少對它作更新操作,那麼為了得到更有效的查詢,通常需要以更新變得緩慢而複雜作為代價。當更新相對於查詢更為頻繁時,反正規化化所節省下的成本則變少了。

例如,假設零件的名稱經常不經常改動,但現有的零件的資料經常變動。這意味著雖然將 PartName 正規化化到 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對非常多 的示例也可以進行反正規化化,通過以下兩種方式:你可以將 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"),hostname : 'goofy.example.com',}
複製程式碼

另一方面,你有可以反正規化化到 1端。假如你想在 host 檔案中儲存一個 host 最近1000條資訊。你可以使用MongoDB 2.4中 介紹的 $each$slice 方法來儲存那隻包含了最近1000 條且已排好序的資訊。

日誌資訊被儲存到了 logmsg 集合中,同時也儲存到了 host 檔案中的反正規化化列表裡。 這樣,即使資訊超出了 hostslogmsgs 陣列,也不會丟失了

 //  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端 的反正規化化都是可以的,

在覺得是否需要反正規化化是,要考慮下面的因素:

  • 你將無法對反正規化化的資料進行原子更新。

  • 在讀較寫的比例高時, 反正規化化才有意義。

下一次,我給你們一些關於這些選項的選擇上的指導。