MongoDB 反正規化的威力
上一次介紹的「MongoDB Schema 設計指南」之後,今天來介紹一下續集趴兔 (原文在此)。在上一篇文章中,已經介紹了一些基本的設計技巧,我們可以透過以下兩個問題幫助我們分析選用適合的 Schema Model。
- 在我們的參照實體中,需要 stand-alone (資料獨立性) 這樣的特性嗎?
- 對於參照實體的基數多寡,可以選定使用 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