先前剛好有機會遇到 MongoDB 亞太地區架構師,發現很多人問的問題都一樣,就是不知道怎麼正確地使用 MongoDB,很多人只是把原本 RDBMS 那一套處理資料的方法搬到 MongoDB,將 Collection 當作 Table 來使用,如果是這樣,其實沒有太大的改變,也沒有獲得太多 NoSQL 的好處。
最近常碰到這樣的問題,到底使用 MongoDB 是否需要進行正規化?資料應該怎麼進行關聯?要如何處理資料散佈在不同 Collection 的 Join 問題?等等...最近剛好看到 MongoDB 官方的這一篇文章「6 Rules of Thumb for MongoDB Schema Design」,讀完以後忽然有些感應,因此寫篇文章介紹一下。
接下來介紹 MongoDB 三種基本設計模式:One-to-Few (少量), One-to-Many (多量) 與 One-to-Squillions (海量)。
第一招:Modeling One-to-Few 少量級關聯模式 (Embedding)
假設我們可以預期某個 Document 中的某個欄位所包含的複數子文件數量不多,可以稱為 One-to-Few 模式。如此「一個實體」對上「少少的實體」,通常的關聯數量可以落在 1000 以內,要注意整體 Document Size 不可以超過 16MB,把關聯的資料直接放在 Document 中的某個欄位,這樣的技巧我們稱為 Embedding。
如下面的例子,一個人可能會有很多地址,但現實狀況一個人不會有擁有非常非常多的地址,大約五筆就算很多了。在以往的關聯式資列庫中,理論上會透過 Normalization (正規化) 將 Address 另外獨立出一張資料表,但是正規化這樣的過程在 DocumentDB 是不需要的,我們反而需要的是 Denormalization (反正規化),藉此提高查詢效率。範例如下:
> 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' } ] }
以上面的例子來說,優點為資料可以保持獨立性 (Stand-alone),且資料不大,一次 Query 出來即可搞定,算是使用 DocumentDB 最基本的反正規化技巧。
第二招:One-to-Many 多量級關聯模式 (Child-Referencing)
假設主實體關聯的子項目數量比起上述的例子來得多一些,但可能在幾百筆上下。但是放在同一個 Document 又有機會超過 16MB 就爆了,另外可能還會考慮到資料獨立性的問題。遇到這樣的情況就是採用類似傳統 RDBMS 的正規化作法,將關聯的 Document 放在另一個 Collection,透過 Object ID 建立關聯,實際查詢時透過 Application-level Join 進行反查。如此便可以滿足大量子物件的一對多關係,並同時保持資料的獨立性。這樣的技巧我們稱為 Child-Referencing,如下:
> db.parts.findOne() { _id : ObjectID('AAAA'), partno : '123-aff-456', name : '#4 grommet', qty: 94, cost: 0.94, price: 3.99 } > 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 ] }
當上述的資料建立後,查詢時可以透過 Application-level Join 完成,像是下面兩個步驟:
// 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() ;
這樣模式的好處在可以維持 stand-alone 特性,當然如果我們直接刪除 parts collection 中的物件,也必須額外刪除 products.parts 中的 ObjectID,好保持資料正確性,不像是關聯資料庫可以很方便地透過 Cascade 完成。
第三招:One-to-Squillions 海量級關聯模式 (Parent-Referencing)
海量級參照模式(大數據的最愛),如果主實體所參照的另一個實體可能是海量級,千以上的極大數字。如果用上面介紹的模式來實作,會遇到用來存放 ObjectID 的陣列爆表。遇到這樣的情況其實很簡單,就是反過來進行參照,實現 Parent-Referencing。像是下面的例子:
> 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 }
實務上可以透過以下的方式進行 Query,比如要找出哪台機器的 log:
// 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()
如何來選擇設計模式?最簡單可以透過估計 Document 的參照「基數」來決定。如果很少就直接放進 Document 做 One-to-Few (Embedding),有點多就將獨立到另一個 Collection 做 Child-Referencing。如要用來存放隨著時間爆炸性成長的資料,就可以選用 Parent-Referencing 模式。依據需求選用適合的設計才是最重要的,看完之後懂了嗎?有沒有覺得自己的 MongoDB Schema 需要調整一下了呢?
有空再來介紹 Part II 更進階的 Schema Design,下次見!
下一篇「MongoDB Schema 設計指南 (Part II) – 反正規化的威力」!