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。