服务端与数据 / 进阶

MongoDB 聚合与建模:先按读取路径决定嵌入还是引用

MongoDB 的灵活结构不等于无需建模。本文以订单统计为例,说明嵌入与引用、聚合管道顺序和 explain 分析。

一起读取、一起更新的数据优先嵌入

订单行项目通常随订单一起读取,数量也有明确上限,适合嵌入。用户的全部历史订单会无限增长,不应塞进用户文档。引用关系适合独立生命周期、高基数或需要单独查询的实体。

嵌入:读取原子、减少关联,但要控制数组和文档大小;引用:实体独立、增长安全,但读取时可能需要额外查询或 $lookup;快照:订单中保存购买时的商品名和价格,避免商品更新改写历史。

尽早过滤,尽晚展开

把能命中索引的 $match 放在管道前面,减少后续阶段的数据量。$project 可以尽早去掉大字段;$unwind 和 $lookup 可能放大记录数,应在过滤之后使用。

EXAMPLE / 02MongoDB
db.orders.aggregate([
  { $match: {
      tenantId: 't_42',
      status: 'paid',
      createdAt: { $gte: ISODate('2026-03-01') }
  }},
  { $group: {
      _id: { $dateTrunc: { date: '$createdAt', unit: 'day' } },
      revenue: { $sum: '$total' },
      orderCount: { $sum: 1 }
  }},
  { $sort: { _id: 1 } }
]);

用 explain 检查是否真的走索引

关注 totalDocsExamined 与 nReturned 的差距、是否出现 COLLSCAN、排序是否溢出内存。报表聚合频繁且数据量大时,考虑增量维护汇总集合,而不是每次扫描全部原始订单。

MongoDB 单文档有 16MB 上限。无限增长数组还会造成更新成本和热点文档问题,应拆成独立集合。

EXAMPLE / 03MongoDB
db.orders.createIndex({
  tenantId: 1,
  status: 1,
  createdAt: 1
});

db.orders.explain('executionStats').aggregate(pipeline);