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 建模不是「把表換成集合」,而是一次范式反轉:以查詢為中心組織資料。嵌入讓熱路徑一次拿全,引用讓大資料獨立演進,索引與聚合管道則把計算下推到儲存層。想清楚「一次要拿到什麼」,模型自然就對了。

本站提供瀏覽器本地工具,免註冊即可試用 →

#MongoDB#文档建模#嵌入式#引用#聚合#索引#NoSQL