MongoDB removes the requirement that every document share an identical shape. It does not remove the consequences of shape. A document model still decides which reads are cheap, which updates are atomic, and which data can grow without bound.
Design from access patterns
Suppose a product page always needs a product and its five latest reviews. Embedding a bounded review preview can make that page one read:
{
_id: 42,
name: "Mechanical Keyboard",
price: 129,
recentReviews: [
{ userId: 7, rating: 5, text: "Solid." }
]
}
The full review history can still live in a separate collection.
In plain terms: store together what the application normally reads together—but only when the embedded part has a clear size and ownership boundary.
Embed when the child belongs to the parent
Embedding fits when related data:
- is normally read with the parent;
- is updated with the parent;
- has bounded growth;
- does not need an independent lifecycle.
One document write is atomic, which can remove cross-document coordination.
Reference when growth or ownership separates
References fit many-to-many relationships, children queried independently, frequently changing shared data, and arrays that could grow indefinitely.
// reviews collection
{ _id: 9001, productId: 42, userId: 7, rating: 5, text: "Solid." }
Embedding every review would make the product document grow continually and rewrite a larger document on each addition. MongoDB documents also have a 16 MiB size limit.
Duplication is a design tradeoff, not a free optimization. If a product name is copied into ten thousand order snapshots, decide whether it is historical truth or data that must be synchronized.
Index the query shape
db.reviews.createIndex({ productId: 1, createdAt: -1 })
db.reviews.find({ productId: 42 })
.sort({ createdAt: -1 })
.limit(5)
.explain("executionStats")
Check the winning plan, totalKeysExamined, totalDocsExamined, and returned count. A low execution time on a tiny collection proves little.
Indexes on arrays become multikey indexes. They are powerful, but wide or multiple arrays can generate many index entries. Index size and write cost still matter.
Flexible does not mean unvalidated
Use schema validation for required fields and types at stable boundaries. Allow deliberate variation, not accidental drift such as price being a number in one document and a string in another.
Decision table
| Condition | Prefer |
|---|---|
| Read and update together, bounded child set | Embed |
| Many-to-many or independent child lifecycle | Reference |
| Shared value changes frequently | Reference or explicit synchronization |
| Historical snapshot must not change | Deliberate duplication |
| Array grows without a known bound | Separate collection |
Review card
- Document shape is an access-path decision.
- Embed bounded data with shared ownership.
- Reference independent or unbounded data.
- Name the consistency rule for every duplicate.
- Validate stable fields even in a flexible model.
- Use
explain("executionStats"), not intuition, to verify indexes.
Sources
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。