MongoDB 文件建模實戰:嵌入式與引用、索引與聚合管道
MongoDB 建模的第一原則
關聯式資料庫先畫 ER 圖、再拆表;MongoDB 反過來——先想清楚「一次查詢要拿到什麼」,再決定怎麼存。它的核心權衡只有一個詞:嵌入(embed)還是引用(reference)。
| 維度 | 嵌入式 | 引用式 |
|---|---|---|
| 讀取 | 一次拿全,快 | 需 $lookup,慢 |
| 寫入 | 大文件更新成本高 | 各自更新,輕 |
| 一致性 | 單文件原子性 | 跨文件需交易 |
| 適用 | 一對少、強一起存取 | 一對多、獨立演進 |
嵌入 vs 引用:怎麼選
嵌入式:部落格與評論(一對少)
{
"_id": "post-1",
"title": "MongoDB 建模",
"comments": [
{ "user": "alice", "text": "很實用", "at": "2026-07-01" },
{ "user": "bob", "text": "收藏了", "at": "2026-07-02" }
]
}
適合「總是一起讀、成長有限」的子資料。一旦評論上千條,文件會膨脹,改用引用 + 獨立集合。
引用式:使用者與訂單(一對多)
// users
{ "_id": "u-1", "name": "Alice" }
// orders
{ "_id": "o-1", "userId": "u-1", "total": 99 }
查詢時用 $lookup 關聯:
db.orders.aggregate([
{ $match: { userId: "u-1" } },
{ $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } }
]);
文件結構複雜時,先拿 JSON 格式化 工具整理範例文件,再決定嵌入層級。
三種經典設計模式
1. Extended Reference(擴展引用)
引用之外,把「經常一起展示」的少量欄位冗餘進主文件,避免 $lookup:
{ "_id": "o-1", "userId": "u-1", "userName": "Alice", "total": 99 }
2. Subset(子集)
把「最近 N 條」嵌入,歷史進獨立集合。適合活動流、通知。
3. Bucket(桶)
把時序/日誌按時間分桶,降低文件數與索引開銷:
{
"_id": "metrics-2026-07-01",
"device": "d-9",
"points": [ { "t": "08:00", "v": 12 }, { "t": "09:00", "v": 15 } ]
}
索引:效能的命門
複合索引與最左前綴
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
查詢必須命中最左前綴才能用到:{userId} ✅,{userId, status} ✅,{status} ❌。
覆蓋查詢(Covered Query)
索引裡就包含所需欄位,連文件都不用回表:
db.orders.createIndex({ userId: 1, total: 1 });
db.orders.find({ userId: "u-1" }, { total: 1, _id: 0 });
TTL 索引自動過期
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });
涉及資料去識別化或簽章時,可用 雜湊摘要 工具產生穩定指紋,避免明文入庫。
聚合管道:ETL 內建
聚合是 MongoDB 最強能力,用階段(stage)串成管道:
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: { _id: "$userId", spend: { $sum: "$total" } } },
{ $sort: { spend: -1 } },
{ $limit: 10 }
]);
常見階段:$match 過濾、$group 分組、$project 投影、$lookup 關聯、$unwind 展開陣列。
若你習慣 SQL,可先用 SQL 格式化 工具理清
GROUP BY思路,再翻譯成$group。
交易與一致性
單文件操作天然原子。跨文件/跨集合需要交易(副本集/分片叢集):
const session = client.startSession();
await session.withTransaction(async () => {
await accounts.updateOne({ _id: "A" }, { $inc: { bal: -100 } }, { session });
await accounts.updateOne({ _id: "B" }, { $inc: { bal: 100 } }, { session });
});
注意:交易有效能成本,能用「嵌入 + 單文件原子」就別上交易。
分片基礎
資料超過單節點容量時依 shard key 水平拆分。選片鍵原則:
- 高基數(取值多),避免「熱點片」。
- 與查詢模式對齊,讓查詢能定位到少數分片。
- 避免單調遞增(如自增 id)導致寫入集中。
常見問題 FAQ
Q1:嵌入還是引用,有沒有量化標準?
經驗值:子文件 < 幾百條、一起讀、不獨立演進 → 嵌入;否則引用。
Q2:陣列無限成長怎麼辦?
用「子集模式」只存最近,或用「桶模式」依時段聚合,避免單文件超 16MB。
Q3:為什麼我的索引沒生效?
多半是沒命中最左前綴,或查詢裡用了 $ne/範圍後又跟等值導致索引無法續用。
Q4:聚合和 MapReduce 選哪個?
永遠優先聚合管道;MapReduce 已基本被取代,僅極特殊場景保留。
Q5:交易很慢怎麼優化?
縮小交易範圍、縮短持鎖時間,或把強一致需求改為「嵌入單文件 + 補償」。
工具推薦
在 MongoDB 建模與維運中,以下 工具庫 能幫上忙:
MongoDB 建模不是「把表換成集合」,而是一次范式反轉:以查詢為中心組織資料。嵌入讓熱路徑一次拿全,引用讓大資料獨立演進,索引與聚合管道則把計算下推到儲存層。想清楚「一次要拿到什麼」,模型自然就對了。
本站提供瀏覽器本地工具,免註冊即可試用 →