← 所有文章2026.06.05 · CS 411 · 資料庫系統

MongoDB 資料建模:Flexible Schema 不等於不用設計

用 access pattern、成長上限、更新 ownership 與 query plan 決定 embedding 或 reference。

MongoDB 不要求每個 document 都有完全相同的形狀,但資料形狀造成的後果仍然存在。Document model 一樣會決定哪些 read 便宜、哪些 update 可以 atomic,以及哪些資料可能無限成長。

從 access pattern 設計

假設商品頁永遠需要 product 和最新五筆 review,可以把有上限的 review preview embed 進 product:

{
  _id: 42,
  name: "Mechanical Keyboard",
  price: 129,
  recentReviews: [
    { userId: 7, rating: 5, text: "Solid." }
  ]
}

完整 review history 仍可放在另一個 collection。

白話來說: Application 經常一起讀的資料,可以考慮放在一起;前提是 embedded 部分有清楚的大小上限和 ownership。

Child 屬於 parent 時考慮 embed

以下條件適合 embedding:

  • Related data 通常和 parent 一起讀;
  • 和 parent 一起更新;
  • 成長有上限;
  • 不需要獨立 lifecycle。

單一 document write 具有 atomicity,因此可能省去 cross-document coordination。

成長或 ownership 分開時使用 reference

Many-to-many、需要獨立查詢的 child、頻繁改動的 shared data,以及可能無限增長的 array,都較適合 reference。

// reviews collection
{ _id: 9001, productId: 42, userId: 7, rating: 5, text: "Solid." }

Embed 所有 review 會讓 product document 不斷變大,每次新增也可能重寫更大的 document。MongoDB document 還有 16 MiB 大小上限。

Duplication 是取捨,不是免費最佳化。若 product name 被複製進一萬筆 order snapshot,要先決定它是不可改的歷史紀錄,還是必須同步的當前資料。

依 query shape 建 index

db.reviews.createIndex({ productId: 1, createdAt: -1 })

db.reviews.find({ productId: 42 })
  .sort({ createdAt: -1 })
  .limit(5)
  .explain("executionStats")

檢查 winning plan、totalKeysExamined、totalDocsExamined 與實際回傳數。小 collection 上的低執行時間不能證明設計正確。

Array index 會成為 multikey index。功能很強,但寬 array 或多個 array 可能產生大量 index entries;index size 與 write cost 仍然存在。

Flexible 不代表不驗證

在穩定邊界使用 schema validation,限制 required field 與 type。允許的是有意義的 variation,不是 price 有時是 number、有時是 string 的意外 drift。

決策表

條件 優先選擇
一起讀寫,child 數量有上限 Embed
Many-to-many 或 child 有獨立 lifecycle Reference
Shared value 經常變更 Reference 或明確同步
Historical snapshot 不應改變 有意識地 duplicate
Array 沒有成長上限 Separate collection

複習卡

  • Document shape 本身就是 access-path 決策。
  • Shared ownership 且有上限的資料適合 embed。
  • 獨立或無上限資料適合 reference。
  • 每份 duplicate 都要說清楚 consistency rule。
  • Flexible model 仍應驗證穩定欄位。
  • 用 explain("executionStats") 驗證 index。

資料來源