MongoDB灵活数据模型允许您从战略上平衡性能和适应性,同时确保数据的一致性和完整性。除了有关 规划模式的一般指导外,还应考虑以下最佳实践来优化数据模型并确定哪种模式设计模式最适合您的应用程序使用案例。
尽早规划模式并进行迭代
规划和设计模式最好在应用程序开发进程的早期完成。在启动应用程序时采用良好的数据建模实践有助于防止随着应用程序的增长而出现性能问题。如果您尽早并适当地遵循数据建模最佳实践,则可以获得更好的性能,并在将来更轻松地扩展应用程序。
注意
根据您的应用程序和优化的重要性,您可能希望在花时间进行优化之前,优先建立一个涵盖基本功能的简单有效的数据模型。
修改数据模型
随着应用程序需求的变化,您应该迭代地设计模式并修改模式。 MongoDB 提供了无需停机即可无缝修改模式的方法。但是,修改生产中使用的大规模模式仍然很困难。
考虑以下示例:
您的任务是构建一个在线用户学习平台的后端,该平台存储有关课程及其课时的信息。最初,平台只需存储每门课程的以下基本信息:
课程标题
讲师
说明
课程,作为子文档大量嵌入每个课程文档中。此时,每个课程子文档仅包含标题、说明和演示文稿幻灯片。
随着平台的发展,每门课程都需要额外的学习和测试格式,例如视频、测验、作业和外部资源链接。将所有这些新数据嵌入每个课程文档会使数据模型变得过于复杂。
相反,请考虑创建一个单独的lessons 集合。这样,您就可以使用引用course lessonsID 链接 和 集合。通过使用引用,每节课都可以包含不同的内容类型,并且可以更灵活地适应未来的格式变化。
链接相关数据
在 MongoDB 中设计数据模型时,请考虑文档的结构以及应用程序使用相关实体中的数据的方式。
要链接相关数据,您可以:
在单个文档中嵌入相关数据。
引用存储在单独集合中的相关数据。
有关何时使用嵌入或引用的示例,请参阅下表:
Scenario | 链接方法 |
|---|---|
将相关数据放在一起将使数据模型和代码更简单。 | 内嵌 |
实体之间存在“has-a”或“contains”关系。 | 内嵌 |
您的应用程序会同时查询多条信息。 | 内嵌 |
您的数据经常一起更新。 | 内嵌 |
您有应同时存档的数据。 | 内嵌 |
关系的子方具有较高的关联基数。 | 引用 |
数据重复过于复杂,无法管理,因此不是首选。 | 引用 |
这些数据的总大小占用了应用程序过多的内存或传输带宽。 | 引用 |
您的嵌入式数据会无限增长。 | 引用 |
您的数据是在写入密集型工作负载中的不同时间写入的。 | 引用 |
对于关系的子端,您的数据可以在没有父项的情况下独立存在。 | 引用 |
要详细学习;了解每种数据链接方法的使用案例、性能注意事项和优势,请参阅:
Duplicate Data
在单个文档中嵌入相关数据时,可能会在两个集合之间重复数据。复制数据使您的应用程序可以在单个查询中查询多个实体的相关信息,同时在逻辑上分离模型中的实体。
在复制数据之前,请考虑以下因素:
数据重复时,读取的性能优势。复制数据可以消除跨多个集合执行联接的需要,从而提高应用程序性能。
需要更新重复数据的频率。处理不频繁更新所需的额外的逻辑比在读取操作上执行联接(查找)的成本更低。但是,频繁更新重复数据可能会导致繁重的工作负载和性能问题。
下表列出了模式中可能存在的不同类型的重复数据:
重复数据类型 | 说明 | 例子 |
|---|---|---|
不可变 | 永不改变的数据。不可变数据是数据复制的良好候选者 | 用户帐户的创建日期。 |
时态 | 可能随时间变化的数据,但保留数据的历史价值非常重要。 | 客户下订单时的解决。如果客户移动到新解决,不会影响之前订单的发货记录地址。 |
对陈旧性敏感 | 需要频繁更新以确保所有数据出现都一致的数据。应用程序可以使用事务或触发器来更新所有事件。 | 给定产品的股票数量,每次客户对该产品下订单时都需要更新。 |
对陈旧性不敏感 | 可以容忍较长时间内过时的数据。应用程序可以使用背景作业,根据可接受的陈旧度和更新费用,定期更新所有出现的数据。 | 为客户推荐的产品列表,无需在每次将新产品添加到系统时都重新计算。 |
如果不需要经常更新重复数据,则只需很少的额外工作即可保持两个集合的一致。但是,如果重复数据经常更新,则使用引用链接相关数据可能是更好的方法。
有关复制相关数据如何帮助优化数据模型的示例,请参阅处理重复数据。
实施数据一致性
如果模式中有重复数据,则需要决定如何在多个集合中保持数据一致。示例,电子商务平台可能需要不断更新数据,以提供其产品股票的实时状态。另一方面,为长期战略决策处理数据的应用程序(例如社交媒体分析)可以容忍读取稍微陈旧的数据。
您可以使用以下任一方法在应用程序中实施数据一致性:
使用验证规则执行模式
模式验证需求取决于用户使用应用程序的方式。 MongoDB 灵活的模式使您可以轻松地改进数据模型,尤其是在开发的早期阶段。但是,随着数据模型的稳定,模式验证可有效确保数据达到预期效果。模式验证对于已建立的应用程序最有用,在该应用程序中,您可以很好地了解如何组织数据。
注意
模式验证规则也很灵活,因此不需要覆盖文档中的每个字段,除非应用程序要求这样做。
您可以在以下场景中使用模式验证:
对于
events集合,确保start_date字段仅存储为日期而不是字符串,以便连接应用程序不会使用意外的类型。对于
store集合,请确保accepted_credit_cards字段属于您的存储接受的信用列表,例如["Visa", "MasterCard", "American Express"]。此验证可防止用户输入不支持的信用金额。对于学生集合,请确保
gpa字段始终为正点。此验证可防止数据输入期间出现错误。
对常用查询字段进行索引
在设计数据模型时,请考虑如何访问权限和存储数据。如果您经常查询、过滤、排序或连接特定字段,请考虑在这些字段上创建索引。通过索引, MongoDB可以:
更快地返回查询结果
更高效地对结果进行排序
优化
$lookup和$group操作减少 CPU 和 I/O 使用量
随着应用程序的增长,监控部署的索引使用情况,以确保索引仍然支持相关查询。
创建索引时,请考虑以下索引行为:
每个索引至少需要 8 kB 的数据空间。
添加索引会对写入操作的性能产生一定的负面影响。对于具有高写入读取比率的集合,索引的成本很高,因为每次插入还必须更新任何索引。
具有高读写比的集合通常受益于额外索引。索引不会影响未编制索引的读取操作。
处于活动状态时,每个索引都会占用磁盘空间和内存。这种资源占用可能很会显著,应跟踪以进行容量规划,特别是要考虑到工作集大小的问题。
其他注意事项
开发数据模型时,结合以下注意事项分析应用程序的所有读写操作。
原子性(Atomicity)
在MongoDB中,写入操作在单个文档级别上是原子性的,即使该操作修改单个文档中的多个嵌入式文档也是如此。这意味着,如果更新操作影响多个子文档,则所有这些子文档都会更新,或者操作完全失败并且不会发生更新。
使用嵌入式文档和数组的非规范化数据模型将所有相关数据合并在单个文档中,而不是跨多个文档和集合进行规范化。此数据模型支持原子操作,而在规范化模型中,操作会影响多个文档和集合。有关为单个文档提供原子更新的示例数据模型,请参阅原子操作的数据模型。
对于存储相关数据之间引用关系的数据模型,应用程序必须发出单独的读取和写入操作来检索和修改这些相关数据。
对于需要对多个文档(在单个或多个集合中)原子性读取和写入的情况,MongoDB 支持分布式事务,包括副本集和分片集群上的事务。
有关详细信息,请参阅事务。
重要
在大多数情况下,与单文档写入操作相比,分布式事务会产生更高的性能成本,并且分布式事务的可用性不应取代有效的模式设计。在许多情况下,非规范化数据模型(嵌入式文档和数组)仍然是数据和使用案例的最佳选择。换言之,对于许多场景,适当的数据建模将最大限度地减少对分布式事务的需求。
有关其他事务使用注意事项(如运行时间限制和 oplog 大小限制),另请参阅生产注意事项。
数据生命周期管理
数据生命周期管理是指管理数据从创建、存储到存档和删除的进程。为了确保模式的成本效益、性能和安全性,请在制定数据建模决策时考虑数据生命周期管理。
如果您的应用程序需要某些数据在数据库中保留一段有限的时间,请考虑使用“生存时间”或“TTL”功能。示例, TTL集合可用于管理 Web应用程序上的用户登录会话,其中会话设立为在30 分钟不活动后自动过期。这意味着MongoDB会在指定时间段后自动删除会话文档,从而帮助您保持会话集合较小,从而实现高效查询。
此外,如果您的应用程序只使用最近插入的文档,可以考虑固定大小集合。固定大小集合可对插入的文档进行先入先出 (FIFO) 管理,并有效地支持根据插入顺序插入及读取文档的操作。
硬件约束
在设计模式时,请考虑部署的硬件,尤其是可用的 RAM。较大的文档使用更多的 RAM,这可能会导致您的应用程序从磁盘读取数据并降低性能。在设计模式时,请尽可能让查询仅返回相关字段。这种做法可确保应用程序的工作集不会不必要地变大。
小文档
每个 MongoDB 文档都包含一定量的开销。这一开销通常微不足道,但如果所有文档都只有几个字节,那么开销就变得很大;比如说,如果集合中的文档只有一两个字段,则可能会出现这种情况。
请考虑按照以下建议和策略来优化这些集合的存储利用率:
显式使用
_id字段。MongoDB 客户端会自动向每个文档添加一个
_id字段,并为_id字段生成一个唯一的 12 字节 ObjectId。此外,MongoDB 始终会为_id字段建立索引。对于较小的文档,这可能会占用大量空间。为了优化存储使用,可以在将文档插入到集合时显式指定
_id字段的值。此策略允许应用程序将值存储在_id字段中,而该值在文档其他部分中会占用空间。您可以将任何值存储在
_id字段中,但由于该值用作集合中文件的主键,因此它必须唯一地标识它们。如果字段的值不是唯一的,那么该值不能用作主键,因为集合中会发生冲突。使用更短的字段名称。
注意
虽然缩短字段名称可以减小MongoDB中的BSON大小,但修改整个文档模型以减小BSON大小通常更有效。 缩短字段名称可能会降低表达能力,并且不会影响索引的大小,因为索引具有不包含字段名称的预定义结构。
MongoDB 会在每个文档中存储所有字段名称。对于大多数文档,这仅占文档使用空间的一小部分;然而,对于小文档来说,字段名称可能在其使用空间中占较大的部分。以下面这个集合为例,其中包含一些如下所示的小文档:
{ last_name : "Smith", best_score: 3.9 } 如果将名为
last_name的字段缩短为lname,并将名为best_score的字段缩短为score,如下所示,则每个文档可以节省 9 个字节。{ lname : "Smith", score : 3.9 } 嵌入文档。