MongoDB 建模时嵌入文档和引用怎么选?
简化版
MongoDB 建模时,如果数据经常一起读取、生命周期一致、子数据规模可控,优先考虑嵌入文档;如果数据会独立增长、被多个对象共享、需要单独查询或频繁独立更新,就考虑引用。选择标准不是像关系型数据库那样先规范化,而是围绕访问模式、数据规模和一致性要求。
详细版
常见选择:
| 方式 | 适合场景 | 风险 |
|---|---|---|
| 嵌入文档 | 一对一、一对少、整体读取 | 文档过大、数组无限增长 |
| 引用 | 一对多、多对多、独立生命周期 | 多次查询、应用层组装 |
例如用户的收货地址数量有限,可以嵌入:
{
name: "Tom",
addresses: [
{ city: "Beijing", detail: "Road 1" }
]
}
但用户订单会持续增长,通常不适合全部嵌入用户文档,而应该单独建 orders 集合,用 userId 引用用户。
完整版教学
一、MongoDB 建模不是越嵌套越好
很多人刚用 MongoDB,会把所有相关数据都塞进一个大文档。这样初期查询很爽,但数据增长后容易出问题。
MongoDB 单文档有大小限制,而且大数组频繁增长会影响更新、索引和网络传输。文档模型强调聚合数据,但聚合边界要合理。
二、嵌入文档适合强聚合关系
嵌入适合这些情况:
- 子数据离开父文档没有独立意义;
- 读父对象时总是需要子数据;
- 子数据数量有限;
- 子数据和父数据生命周期一致;
- 需要单文档原子更新。
例如商品规格、文章标签、用户少量配置项,都可以嵌入。
单文档更新在 MongoDB 中是原子的,这也是嵌入设计的重要收益。
三、引用适合独立实体
引用适合这些情况:
- 子数据数量可能无限增长;
- 子数据经常单独查询;
- 子数据被多个父对象共享;
- 子数据更新频率和父对象不同;
- 需要独立分片或索引策略。
例如用户和订单、作者和文章、商品和评价,通常更适合拆成不同集合,通过 ID 建立引用关系。
四、不要照搬关系型范式
关系型数据库常用三范式减少冗余,MongoDB 则更关注读写路径。如果每次展示订单都需要用户名、商品名、下单快照,可以适度冗余。
例如订单文档里保存商品下单时的名称和价格:
{
orderNo: "O1001",
productId: ObjectId("..."),
productName: "机械键盘",
priceAtOrder: 399
}
这不是错误冗余,而是历史快照需求。
五、引用后的 JOIN 怎么办
MongoDB 可以使用 $lookup 做集合关联,但它不应该成为所有查询的默认方案。大量跨集合关联会让 MongoDB 失去文档模型的优势。
常见工程做法:
- 高频展示字段适度冗余;
- 主数据通过引用保持关系;
- 后台异步同步冗余字段;
- 查询层按 ID 批量加载,避免 N+1;
- 对复杂报表使用专门分析库。
六、规模边界要用数字判断
嵌入还是引用,不能只看关系是一对多,还要看“一对多到底有多少”。一个用户 3 到 5 个收货地址,嵌入非常自然;一个用户 10 万条订单,嵌入用户文档就会让文档持续增长、更新变重、查询和网络传输都变差。
| 关系规模 | 更常见选择 | 理由 |
|---|---|---|
| 一对一 | 嵌入 | 生命周期一致,读取简单 |
| 一对少 | 嵌入 | 子数据规模可控 |
| 一对多且持续增长 | 引用 | 避免大数组和文档膨胀 |
| 多对多 | 引用或关系集合 | 独立查询和共享更自然 |
比如订单评论如果每单最多几十条,可以嵌入订单或单独建评论集合都能讨论;但商品评价可能达到百万级,就必须独立集合、独立索引和分页查询。
七、常见误区与追问
| 判断问题 | 偏向嵌入 | 偏向引用 |
|---|---|---|
| 是否总是一起读 | 是 | 否 |
| 子数据是否无限增长 | 否 | 是 |
| 是否被多个父对象共享 | 否 | 是 |
| 是否需要单独查询排序 | 否 | 是 |
记忆钩子:嵌入解决“一次读完整对象”,引用解决“独立增长和独立查询”。不要用关系型范式硬套,也不要把所有东西塞成巨型文档。
- 误区:MongoDB 建模就是尽量嵌入。 嵌入适合强聚合和小规模子数据,无限增长数组和独立实体更适合引用。
- 误区:引用就等于关系型数据库设计。 MongoDB 中引用可以配合冗余快照、批量加载和异步同步,不一定照搬三范式。
- 误区:
$lookup可以弥补所有建模问题。 高频复杂关联会让查询成本变高,应该优先从文档边界和冗余字段上优化。 - 追问:为什么嵌入有单文档原子性优势? MongoDB 单文档写入是原子的,把强一致的小对象嵌在一起可减少多文档事务需求。
- 追问:订单里的商品名为什么可以冗余? 它通常是下单时快照,商品改名后历史订单仍应显示当时名称。
- 追问:如何避免引用后的 N+1 查询? 可以按 ID 批量查询、冗余高频展示字段,或针对复杂查询建立读模型。
八、加强记忆
MongoDB 嵌入和引用的判断线是:一起读、一起活、数量小,就嵌入;独立查、独立长、被共享,就引用。文档边界不是按表关系画,而是按业务访问模式和增长规模画。