选择正确 MongoDB 数据模型:嵌入式文档 vs. 引用文档
根据访问模式、数据增长、一致性和更新行为,选择嵌入式或引用式 MongoDB 文档。
选择合适的 MongoDB 数据模型:嵌入式文档与引用式文档
MongoDB 数据建模通常归结为一个实际问题:相关数据应该放在同一个文档中,还是一个文档引用另一个文档?这个选择会影响读取速度、更新成本、文档增长以及应用程序需要处理的一致性工作。
本指南深入探讨了在父文档中嵌入文档与跨不同集合引用相关文档之间的权衡。理解何时以及如何应用这些技术,将使您能够设计高效、高性能的 MongoDB 模式,以满足应用程序特定的访问模式。
理解 MongoDB 数据建模策略
MongoDB 将数据组织成文档(类似于 JSON 对象),存储在集合中。这些文档之间的关系可以使用两种核心模式进行建模:
- 嵌入(反规范化): 将相关数据直接存储在父文档内部。
- 引用(规范化): 仅存储对另一个集合中相关文档的引用(如
_id),类似于外键。
1. 嵌入模式(反规范化)
嵌入涉及将一个文档直接放置在另一个文档内部。当数据关系是“一对少数”或相关数据经常与父文档一起访问时,这种技术在 MongoDB 中非常受青睐。
何时使用嵌入
在以下情况下使用嵌入模式:
- 数据一起访问: 如果您在查询父文档时几乎总是需要相关数据,嵌入可以最大限度地减少获取完整信息集所需的数据库操作次数。
- 一对少数关系: 适用于嵌入文档数组保持相对较小且可预测的关系(例如,用户的最后 10 次登录活动,或订单的订单项)。
- 数据一致性至关重要: 嵌入的数据本质上是保持一致的,因为它位于单个文档内,简化了 MongoDB 单文档 ACID 事务提供的原子性保证。
嵌入示例
考虑一个 Product 及其 Reviews。如果评论经常与产品一起获取,并且评论总数是可控的:
// 产品集合文档
{
"_id": ObjectId("..."),
"name": "高性能 SSD",
"price": 129.99,
"reviews": [
{
"user": "Alice",
"rating": 5,
"comment": "最快的驱动器!"
},
{
"user": "Bob",
"rating": 4,
"comment": "物超所值。"
}
]
}
嵌入的缺点
- 文档大小限制: MongoDB 文档有 16MB 的最大大小限制。如果嵌入文档数组无限制增长,最终可能需要引用或分桶。
- 更新开销: 大型嵌入数组会使更新成本更高,并可能增加父文档上的争用。
- 数据重复: 如果嵌入的数据需要独立于父文档共享或显示,则存在数据重复的风险,并且如果更新未在所有副本之间同步,则可能导致最终一致性问题。
2. 引用模式(规范化)
引用模仿了关系数据库中外键的概念。您不是嵌入相关数据,而是存储相关文档的 _id 或其他稳定标识符。这通常需要第二次查询、$lookup 聚合阶段或应用程序端连接来检索相关数据。
何时使用引用
在以下情况下使用引用模式:
- 一对多或多对多关系: 当关系的一方可以无限增长时(例如,博客文章上的评论数量,或属于多个组的用户)。
- 数据在多个父文档之间共享: 如果相关数据实体需要被多个其他文档独立更新和访问(例如,许多
Product文档使用的Category文档)。 - 大数据集: 当嵌入会违反 16MB 文档大小限制时。
引用的类型
A. 手动引用(应用程序端连接)
在父文档中存储 _id:
// 作者集合
{
"_id": ObjectId("author123"),
"name": "Jane Doe"
}
// 书籍集合
{
"_id": ObjectId("book456"),
"title": "数据建模 101",
"author_id": ObjectId("author123") // 引用
}
要检索作者姓名,您可以执行两个查询或使用 $lookup:
// 在聚合框架中使用 $lookup 的示例
db.books.aggregate([
{ $match: { title: "数据建模 101" } },
{
$lookup: {
from: "authors", // 要连接的集合
localField: "author_id", // 输入文档(书籍)中的字段
foreignField: "_id", // 'from' 集合(作者)文档中的字段
as: "author_details"
}
}
]);
B. 双向引用
对于双向关系,您也可以在子文档中引用父文档。这使得在两个方向上遍历关系更容易,尽管它增加了写入开销,因为更新必须在两个地方进行。
引用的缺点
- 查询复杂性增加: 检索完全反规范化的数据需要连接(通过应用程序代码或 MongoDB 的
$lookup),这可能比单个嵌入式读取操作慢。 - 一致性管理: 如果您从引用文档中复制了某些字段,例如书籍记录上的作者显示名称,则必须更新这些副本或接受暂时的过时数据。
总结:做出正确的选择
嵌入和引用之间的决定围绕着访问模式。问问自己:相关数据多久被检索一次?它多久更改一次?它是小的还是可能巨大的?
| 特性 / 考虑因素 | 嵌入(反规范化) | 引用(规范化) |
|---|---|---|
| 读取性能 | 优秀(单次查询) | 良好到一般(需要连接) |
| 写入性能 | 对于大型或热门的父文档可能成本高昂 | 对于独立文档通常更简单 |
| 数据大小限制 | 受限于 16MB 文档限制 | 避免单个巨大的父文档,但仍需仔细设计索引和查询限制 |
| 关系类型 | 一对少数 | 一对多,多对多 |
| 数据一致性 | 高(原子写入) | 手动管理(可能过时) |
最佳实践提示:从嵌入开始,之后调整
一个常见且有效的策略是从嵌入您知道经常一起读取的数据开始。这针对常见情况进行了优化。如果您后来遇到由于文档增长过大或更新复杂性过高而导致的性能瓶颈,您可以将该特定数据调整到其自己的集合中,并切换到引用。
要点
MongoDB 模式在匹配您的实际查询时效果最佳。嵌入您一起读取且可以保持有界的数据。引用独立增长、由多个父文档共享或按自己的计划更改的数据。在确定模型之前,写下您的主要读取和写入操作,然后使用实际的文档大小测试这些路径。