在 CosmosDB 中建模数据:从 SQL 范式转变





5.00/5 (1投票)
如何在具有 SQL 背景的情况下为 Azure CosmosDB 建模数据?
引言
面向文档的数据库是一种 NoSQL 数据库,以类似 JSON 的格式存储和检索数据。在这些数据库中,数据被组织成文档,文档是键值对或字段值对的集合。每个文档可以包含嵌套结构、数组和其他复杂的数据类型。
-
与关系数据库不同,面向文档的数据库不需要预定义模式,从而在数据结构方面提供了更大的灵活性。这种灵活性在处理动态或半结构化数据时尤其有利。面向文档的数据库通常用于不同记录之间数据可能不同的场景。
-
流行的面向文档的数据库包括 MongoDB、CouchDB 和 Azure Cosmos DB。这些数据库广泛用于现代 Web 应用程序、内容管理系统和其他需要灵活可扩展数据存储的场景。
在本文中,我们将深入探讨核心原理,并演示从关系范式到面向文档范式的转变。为了说明基本概念,我们将对一个电子商务应用程序进行建模,最初采用类似关系数据库的方式,然后逐步将其迁移到面向文档的方法。
我们参考了以下书籍来阐释某些概念。
本文改编自一篇最初发布在此处的帖子:链接。
数据建模的传统方法是什么?
开发人员早已习惯于使用关系数据库进行数据建模。这种范式易于理解,并且实际上可以让我们轻松地解决许多问题。它由 Codd 在七十年代提出,此后得到了广泛传播,成为一项基石技术,常常不假思索地与 SQL 衍生产品联系在一起。然而,自 2000 年代末以来,为了有效管理海量数据,新的概念应运而生,引入了新的数据建模方式。将思维模式转移到这种新方法可能具有挑战性,我们的目标是在这里阐明所有相关的概念。
传统上,数据使用关系数据库进行建模,涉及几个关键步骤。
- 识别我们系统中的主要实体(我们需要存储信息的对象或概念,例如客户、订单、产品)。
- 定义实体之间的关系,并确定实体之间如何相互关联(例如,客户下单,订单包含产品等)。
- 应用规范化技术以消除冗余并提高数据完整性。规范化表可确保高效存储并防止不必要的重复。
现在我们将看到一个实际的例子。
如何在关系数据库中为电子商务应用程序建模?
我们将从一个大家熟悉的简单示例开始:一个在线商店,人们可以在其中下单、查看产品和撰写评论。从关系的角度来看,我们首先需要定义实体以及不同概念之间的关系。让我们简要回顾一下。
产品
产品是在线商店中销售的商品。它们主要以其 ID、名称、描述、单价、图片、URL 和类别为特征。
功能 | 类型 |
ID | 唯一标识符 |
名称 | 字符串 |
描述 | 字符串 |
单价 | decimal |
图片 URL | 字符串 |
Url | 字符串 |
类别 ID | 唯一标识符 |
... | ... |
Customers
客户是在线商店购买产品的个人。他们以其 ID、姓名、地址和其他基本数据为特征。
功能 | 类型 |
---|---|
ID | 唯一标识符 |
名称 | 字符串 |
国家 | 字符串 |
... | ... |
订单
订单由客户下单购买产品。它们以其参考号和订单行列表为特征。
功能 | 类型 |
---|---|
参考 | 唯一标识符 |
CustomerId | 唯一标识符 |
Lines | 列表 |
... | ... |
评估
对产品的评论通常是客户对产品进行的书面评估或评价。它通常包括有关产品功能、性能、质量和整体满意度的反馈和评分。
功能 | 类型 |
---|---|
评论 ID | 唯一标识符 |
CustomerId | 唯一标识符 |
ProductId | 唯一标识符 |
注释 | 字符串 |
评分 | 整数 |
... | ... |
一旦在关系数据库(例如 MySQL 或 SQL Server)中实现,实体和关系的外观可能如下所示:
以下是该模式的一些基本方面:
- 它包含 8 个表。通常,在 SQL 中,为每个新概念或概念之间的关系创建一个表是一种标准做法,并且某些数据库可能包含数百甚至数千个表。
- 每个表都遵循预定义的严格模式,包含结构化数据。这强制执行参照完整性。
- SQL 提供了查询各种数据的能力。虽然某些查询会执行得非常快,但其他查询可能会慢得不合理。
- SQL 是一个事务性存储,允许数据存储保持一致性,尽管这可能会导致偶尔的性能权衡。
- 可伸缩性可能会成为瓶颈。
我们的目标是将此 SQL 模式迁移到面向文档的数据库模式。需要注意的是,我们并非评判 SQL 是否优于或劣于其面向文档的对应产品。我们的目标是说明如何从一种模型迁移到另一种模型,同时突出每种模型的关键方面。
什么是面向文档的数据库?
面向文档的数据库是一种 NoSQL 数据库,它旨在存储、检索和管理半结构化或非结构化数据,通常以文档的形式。与存储具有预定义模式的表中的数据的传统关系数据库不同,面向文档的数据库将数据存储在灵活的、无模式的文档中。
什么是 Azure Cosmos DB?
-
Azure Cosmos DB 是微软提供的多模型、全球分布式数据库服务。它是 Azure 数据库产品的一部分,旨在使开发人员能够构建高度响应和可扩展的应用程序,并实现全球范围内的数据低延迟访问。Azure Cosmos DB 支持多种数据模型,包括文档、键值、图、列族和宽列存储,使其成为一个多模型数据库。在这里,我们仅打算使用该数据库的面向文档的功能。
-
Azure Cosmos DB 支持各种数据模型,它旨在水平扩展吞吐量和存储,允许您根据需求向上或向下扩展资源,自动为所有数据建立索引,提供高效、高性能的查询功能。
这是微软提出的业务主题(当然,并非空穴来风,我们稍后将探讨这些功能的重要意义以及微软为何如此强调它们)。然而,在本文中,我们将依赖于分区的关键概念,这将使我们能够构建高度可扩展且高效的应用程序。
什么是分区?
在 Azure Cosmos DB 中,分区是一个基本概念,它涉及将我们的数据分布到多个物理分区以实现可伸缩性和性能。
-
物理分区是 Cosmos DB 服务中的底层存储结构。每个物理分区是物理资源分配(计算、存储和吞吐量)的单元。目标是将预配的吞吐量和数据存储均匀地分布在这些物理分区上。随着我们的数据和吞吐量需求的增加,Azure Cosmos DB 将自动添加更多物理分区来分发负载。
-
逻辑分区是数据的单元,并且是共享相同分区键值的项的容器。分区键是用户在创建容器时选择的属性。它用于确定项属于哪个逻辑分区。分区键的选择至关重要,因为它会影响数据在物理分区上的分布,并可能影响查询性能。
非常重要 1
逻辑分区内的所有项都保证驻留在同一个物理分区上。
非常重要 2
跨多个分区查询(跨分区查询)可能会导致更高的延迟并消耗更多吞吐量,因为它们需要跨多个物理分区进行协调。
给我举个例子!
这一切都有些繁琐、乏味且冗长,所以让我们用一个简洁的例子来简化。想象一下我们需要存储有关汽车型号的信息。
汽车将存储在 Azure Cosmos DB 的一个容器中,并且我们必须主动在容器创建过程中指定分区键。一个可行的选择是利用汽车型号等属性来实现此目的。
一个或多个逻辑分区可以映射到一个物理分区。单个物理分区可以容纳一个或多个逻辑分区,反之亦然,一个或多个逻辑分区可以关联到一个单个物理分区。但是,每个单独的逻辑分区将始终对应一个且仅一个物理分区。
按汽车型号查询
考虑一个目标是检索特定汽车型号的场景。查询很简单,我们的模型也经过精心设计,可以高效地支持它。在这种情况下,只会访问一个逻辑分区,从而确保高效率。
按国家/地区查询
现在,设想一下我们要检索与某个特定国家/地区(例如日本)相关的所有汽车的情况。鉴于国家/地区属性分布在多个分区中,这种情况就是一个跨分区查询的例子。
跨分区查询本身并非有害,有时可能无法避免。然而,频繁执行它们或涉及许多访问的分区可能会对性能产生不利影响。尤其要谨慎,因为 Azure Cosmos DB 的定价模型与 CPU 使用率挂钩——计算资源使用量的增加与成本的增加相关。
这些考虑因素引导我们得出以下一般性指导。
非常重要
在设计我们的 Cosmos DB 数据库时,应根据我们的访问模式和查询要求仔细选择正确的分区键。好的分区键可以将数据均匀地分布在分区之间,并最大限度地减少跨分区查询。分区是 Azure Cosmos DB 提供弹性可伸缩性和高性能数据访问的关键功能,使其适用于全球分布和高要求应用程序。
作为开发人员,我们的最终责任将是仔细地将数据划分为经过深思熟虑的分区,从而需要仔细设计容器。
但如何做到这一点?
信息
当然,作为开发人员,并非总能提前预测所有潜在的查询。用户交互和需求的动态性可能会引入不可预见的查询。
事实上,虽然无法预见所有可能的查询或不可预见的需要,但主动考虑最重要和最常发生的查询是一种务实的方法。用户通常理解这种权衡,承认一些不太频繁的查询效率可能不高。在工程领域,这种权衡被视为折衷,反映了在优化常见用例和为不常见场景保持灵活性之间取得平衡。
绝对正确,我们在这里触及了面向文档的数据库以及 NoSQL 范式的基本方面。与结构更严格、更侧重于模式的关系数据库不同,在 NoSQL 数据库(尤其是面向文档的数据库)中,需要预先考虑查询。NoSQL 数据库提供的灵活性伴随着设计数据结构和选择与预期查询和使用模式相匹配的分区键的责任。这种主动考虑可以提高处理各种数据访问需求的性能和可伸缩性。
总结
为了优化性能,在设计数据模型和查询时,考虑分区策略至关重要。
具体来说呢?
当我们考虑前面讨论的场景时,我们应该如何处理分区?决定取决于识别最频繁的查询,而这实际上又取决于特定应用程序的需求。最终,解决方案可能涉及创建两个容器,每个容器采用自己的分区策略——一个用于汽车型号,另一个用于国家/地区。应用程序将负责确定适当的容器,从而简化流程,而无需我们进行长时间的深思熟虑。
现在,设想一下我们需要编译与每种汽车型号相关的所有经销商列表的场景,例如识别哪家经销商正在销售雷诺 Clio V 或福特蒙迪欧。在这种情况下,使用具有以模型为中心的策略的容器将非常有利。好消息是,我们已经拥有这样一个容器!所有需要做的就是将我们的经销商添加到这个现有容器中。
现在,在按型号分区的容器中,我们发现有两种类型的实体:指定的汽车和各种经销商。但是,挑战在于区分它们。解决方案很简单:通过在每个项中引入新属性。
重要
这就是微软的商业推销变得至关重要并完成闭环的地方:为所有属性启用自动索引,可以快速查询特定分区内的内容(如我们在类型属性案例中所见),同时又允许在同一容器中存储无模式数据,涵盖经销商和汽车。
我们可以做得更好吗?
在将经销商整合到我们的应用程序时,一种替代方法可能涉及将每个经销商直接与相应的汽车项相关联。
这种方法符合面向文档的范例,并可被视为一种反规范化形式。虽然这本身并非有问题,但仍需谨慎。如果经销商的数量庞大(在我们的示例中很可能是这样),采用这种方法会导致每个汽车的项过大,消耗大量带宽。因此,这种方法最适合相关对象列表保持合理大小的场景。
是时候在实践中实施这些概念了:这一点将在这里进行详细讨论。
历史
- 2023 年 12 月 12 日:初始版本