如何选择合适的MongoDB数据模型:嵌入式文档与引用式文档
MongoDB作为一种文档数据库,其灵活性允许开发人员以多种方式建模数据之间的关系。与严格强制规范化模式的传统关系型数据库不同,MongoDB提供了两种主要且强大的策略来组织集合中的相关数据:嵌入(Embedding)和引用(Referencing)。选择正确的方法至关重要,因为它直接影响应用程序性能、数据一致性、查询复杂性和可扩展性。
本指南深入探讨了将文档嵌入到父文档中以及在不同集合之间引用相关文档之间的权衡。了解何时以及如何应用这些技术将使您能够设计出高效、高性能的MongoDB模式,以适应应用程序特定的访问模式。
理解MongoDB数据建模策略
MongoDB将数据组织成存储在集合中的文档(类似于JSON对象)。这些文档之间的关系可以使用两种核心模式进行建模:
- 嵌入(Embedding)(非规范化):将相关数据直接存储在父文档内部。
- 引用(Referencing)(规范化):仅存储对另一个集合中相关文档的引用(例如
_id),类似于外键。
1. 嵌入模式(非规范化)
嵌入涉及将一个文档直接放置在另一个文档内部。当数据关系为一对少(one-to-few)或相关数据经常与父文档一起访问时,MongoDB非常青睐这种技术。
何时使用嵌入模式
在以下情况使用嵌入模式:
- 数据一起访问:如果您在查询父文档时几乎总是需要相关数据,嵌入可以最大限度地减少获取完整信息集所需的数据库操作数量。
- 一对少关系(One-to-Few Relationships):非常适用于嵌入式文档数组保持相对较小且可预测的关系(例如,用户的最后10次登录活动,或订单的行项目)。
- 数据一致性至关重要:嵌入式数据本质上是一致的,因为它驻留在单个文档中,这简化了MongoDB单文档ACID事务提供的原子性保证。
嵌入示例
考虑一个Product及其Reviews。如果评论经常随产品一起获取,并且评论总数是可管理的:
// Product Collection Document
{
"_id": ObjectId("..."),
"name": "High-Performance SSD",
"price": 129.99,
"reviews": [
{
"user": "Alice",
"rating": 5,
"comment": "Fastest drive ever!"
},
{
"user": "Bob",
"rating": 4,
"comment": "Great value."
}
]
}
嵌入的缺点
- 文档大小限制:MongoDB文档的最大大小限制为16MB。如果嵌入式文档数组无限增长,您最终将达到此限制,需要转向引用。
- 更新开销:更新单个嵌入元素需要重写整个父文档,如果父文档非常大,这可能会效率低下。
- 数据重复:如果嵌入数据需要独立于父文档共享或显示,那么如果更新不同步到所有副本,您将面临数据重复和最终一致性问题的风险。
2. 引用模式(规范化)
引用模仿了关系型数据库中外键的概念。您不是嵌入相关数据,而是在父文档中存储相关文档的_id(或ID的组合)。这需要第二次查询($lookup聚合阶段或应用程序端连接)来检索实际的相关数据。
何时使用引用模式
在以下情况使用引用模式:
- 一对多或多对多关系:当关系的一方可以无限增长时(例如,博客文章的评论数量,或属于多个组的用户)。
- 数据在多个父文档之间共享:如果相关数据实体需要由多个其他文档独立更新和访问(例如,由许多
Product文档使用的Category文档)。 - 大数据集:当嵌入会违反16MB文档大小限制时。
引用的类型
A. 手动引用(应用程序端连接)
在父文档中存储_id:
// Author Collection
{
"_id": ObjectId("author123"),
"name": "Jane Doe"
}
// Book Collection
{
"_id": ObjectId("book456"),
"title": "Data Modeling 101",
"author_id": ObjectId("author123") // Reference
}
要检索作者姓名,您可以执行两次查询或使用$lookup:
// Example using $lookup in the aggregation framework
db.books.aggregate([
{ $match: { title: "Data Modeling 101" } },
{
$lookup: {
from: "authors", // Collection to join
localField: "author_id", // Field from the input documents (books)
foreignField: "_id", // Field from the documents of the 'from' collection (authors)
as: "author_details"
}
}
]);
B. 双向引用
对于双向关系,您也可以在子文档中引用父文档。这使得在两个方向上遍历关系更容易,尽管它增加了写入开销,因为更新必须在两个地方发生。
引用的缺点
- 查询复杂性增加:检索完全非规范化的数据需要连接(通过应用程序代码或MongoDB的
$lookup),这可能比单个嵌入式读取操作慢。 - 一致性管理:如果您更改了引用数据(例如,重命名作者),您必须手动更新所有引用该作者的文档,或者接受某些文档会显示陈旧数据直到它们被刷新。
总结:做出正确的选择
嵌入和引用之间的决定取决于访问模式。问问自己:这些相关数据多久检索一次?多久更改一次?它是小规模的还是潜在的大规模的?
| 特性/考量因素 | 嵌入(非规范化) | 引用(规范化) |
|---|---|---|
| 读取性能 | 优秀(单次查询) | 良好到一般(需要连接) |
| 写入性能 | 较差(整个文档重写) | 良好(仅更新引用点) |
| 数据大小限制 | 限于16MB | 无实际限制 |
| 关系类型 | 一对少 | 一对多,多对多 |
| 数据一致性 | 高(原子写入) | 手动管理(可能出现陈旧数据) |
最佳实践提示:先嵌入,后调整
一个常见且有效的策略是首先嵌入您知道会经常一起读取的数据。这优化了常见情况。如果您后来由于文档增长过大或更新复杂性过高而遇到性能瓶颈,您可以将该特定数据调整到自己的集合中,并切换到引用模式。
结论
MongoDB提供了根据应用程序需求优化读取或写入的灵活性。当数据紧密耦合时,嵌入以牺牲更新简单性为代价,实现快速读取访问。引用则保留了数据完整性并处理了无限增长,但代价是涉及连接的读取操作更加复杂。通过仔细分析应用程序的读写比和关系基数,您可以架构一个能够最大限度地提高性能和可维护性的MongoDB模式。