fbpx

MongoDB Schema 設計指南

mongodb在高喊 NoSQL 萬歲的同時,想想如何設計 MongoDB Schema?其實 NoSQL 不等於 NoSchema,同樣 Document DB 沒有固定的 Schema 也不代表不需要 Schema Design。為了讓資料更好維護,依據業務邏輯設計資料存取的 Schema 是需要的工作。

先前剛好有機會遇到 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) – 反正規化的威力」!

MongoDB 系列文章

  2 comments for “MongoDB Schema 設計指南

發佈留言