fbpx

MongoDB Schema 設計指南 (Part II) - 反正規化的威力

mongodb

MongoDB 反正規化的威力

上一次介紹的「MongoDB Schema 設計指南」之後,今天來介紹一下續集趴兔 (原文在此)。在上一篇文章中,已經介紹了一些基本的設計技巧,我們可以透過以下兩個問題幫助我們分析選用適合的 Schema Model。

  1. 在我們的參照實體中,需要 stand-alone (資料獨立性) 這樣的特性嗎?
  2. 對於參照實體的基數多寡,可以選定使用 one-to-few, one-to-many 或 one-to-squillions 哪一種模式?

如果上述的問題您還沒有非常清楚,那麼建議可以複習一下前一篇文章的介紹。接下來我們要介紹一些進階的 MongoDB Schema 設計方法,介紹利用 Two-Way Referencing (雙向參照) 與 Denormalization (反正規化) 這些技巧來使用 MongoDB,讓我們的查詢更有效率。

Two-Way Referencing (雙向參照設計)

Two-Way Referencing 我也不知道怎麼翻譯,所以先暫時叫做「雙向參照」吧!先回顧一下之前提到的 one-to-squillions (海量級關聯模式),可以快速在海量級母體中查詢資料。但是在兩個關聯實體中,有時候查詢的圍度與面向不同,有時候由 A 查 B,有時候由 B 查 A。這時候我們就需要雙向參照的設計模式,舉個工作單管理的例子如下:

db.person.findOne()
{
    _id: ObjectID("AAF1"),
    name: "Kate Monster",
    tasks [     // array of references to Task documents
        ObjectID("ADF9"), 
        ObjectID("AE02"),
        ObjectID("AE73") 
        // etc
    ]
}

上面儲存每個使用的的資料與目前擁有的工作單 (Tasks),然而每個工作單的集合如下:

db.tasks.findOne()
{
    _id: ObjectID("ADF9"), 
    description: "Write lesson plan",
    due_date:  ISODate("2014-04-01"),
    owner: ObjectID("AAF1")     // Reference to Person document
}

上述工作單 Document 其中有個 owner 欄位,並且指向所屬的擁有者。這時無碖我們想要查詢某個人的工作單,或者由某個工作單查到所屬的使用者,都可以很方便地完成,人員 (person) 與工作單 (tasks) 皆保持資料一致性 (Stand-Alone)。那缺點呢?缺點就是一旦有資料關係需要變更,兩個物件內容都要進行更新,必須手動來同步關聯狀態。

Intermediate (媒介設計模式)

多對一反正規化 (Denormalizing from Many -> One)

這個方法應該是到目前為止,用到「反正規化」概念最深入的模式,主要將一對多模型進行反正規化,減少查詢的 Join 作動以提升效率。先舉個例子說明一下,比如一個「商品」可能由數個「組件」組成,是一個多對多的關係,如下:

> 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
   ]
}

若是我們想要很快地撈出「商品」與他所屬的「組件」名稱,在上述原本已經正規化的狀態下,就必須再去查詢 parts collection 才能獲得「組件名稱」。為了更有效率地進行查詢,可以透過反正規化來完成,我們將資料的儲存方式改成下面的範例:

> 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
   ]
}

上述我們直接在 parts 欄位放上我們常需要的 name 欄位,如此一來只要 Query 一個 Collection 就可以獲得我們想要的資訊,減少 Join 的執行,這樣的方式對於大量資料的查詢是很有幫助的。如此違反正規化的過程就稱為「反正規化」,缺點可以很清楚地發現,因為 name 被重複儲存記錄,維護的時候就很麻煩。反正規劃之後,若是要維持資料一致性,修改 name 欄位就必須更新所有地方。像是上述這樣的例子並不會很常更改「組件」實體的 name 欄位,整體獲得的效率是高過維護成本。

此外,如果要查詢每個組件的其他詳細資料,當然要用到 Join,在 MongoDB 裡面我們稱為 Application-level Join,可以再次撈取另一個 Collection 進行組合,如下:

 // 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()

一對多反正規化 (Denormalizing from One -> Many)

這個例子前面介紹的概念其實差不多,在上述的案例,假設我們想快速查詢「組件」資訊,又想要查詢這些「組件」所屬的「商品」名稱,如果想要加速查訊效率,不想要使用 Application Join,那就是對「商品」的名稱欄位進行反正規化。修改後的 Schema 如下:

> 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
}

上述例子將「商品」的 name 欄位重複設定在「組件」中的 product_name 欄位中,反正規化之後就可以大幅提升查詢效率。當然失去資料的一致性,在維護上要付出的代價是相同的,根據實際使用的情境進行反正規化,在 MongoDB 或其他分散式資料庫都是常見的手法。在進行反正規化之前,我們必須理解以下兩個重要的特性:

  • 反正規劃之後,就會失去資料的一致性 (Stand-Alone)
  • 在讀取頻率高,寫入頻率低的情況下,進行反正規化才有意義

系統需求經過分析之後,將更新頻率不高的欄位進行反正規劃,會是不錯的選擇。在 Query 的便利性與效率會大大提昇,是不是很有趣呢?今天就先介紹到這,下次再見囉。YY

MongoDB 系列文章

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料