尽管无架构数据库(如 Azure Cosmos DB)可以轻松存储和查询非结构化和半结构化数据,但考虑数据模型以优化性能、可伸缩性和成本。
如何存储数据? 应用程序如何检索和查询数据? 应用程序是读取密集型还是写入密集型?
阅读本文后,可以回答以下问题:
- 什么是数据建模,我为什么应该关注?
- Azure Cosmos DB 中的数据建模与关系数据库有何不同?
- 如何在非关系数据库中表达数据关系?
- 我应何时嵌入数据和何时链接数据?
JSON 格式的数字
Azure Cosmos DB 将文档保存在 JSON 中,因此在将数字存储在 JSON 中之前,请务必确定是否将数字转换为字符串。 如果数字可能会超过IEEE 754 binary64定义的双精度浮点数的边界,请将所有数字转换为String。
JSON 规范解释了为何使用此边界之外的数字是一种不良做法,因为互作性问题。 这些问题尤其与分区键列相关,因为它不可变,并且需要数据迁移才能在以后进行更改。
嵌入数据
在 Azure Cosmos DB 中为数据建模时,请将实体视为以 JSON 文档表示的 自包含项 。
为了进行比较,让我们先了解一下关系数据库中的数据建模方式。 下面的示例演示了如何在关系型数据库中存储一个人的信息。
使用关系数据库时,策略是将所有数据规范化。 规范化数据通常涉及到将一个实体(例如某人)的信息分解为多个离散的组成部分。 在示例中,一个人可以有多个联系人详细信息记录,以及多个地址记录。 可以通过提取常见字段(如类型)来进一步细分联系人详细信息。 相同的方法适用于地址。 每个记录都可以归类为 家庭 或 企业。
规范化数据时的指导前提是避免在每个记录中 存储冗余数据 ,而是引用数据。 在本示例中,若要读取某个人的所有联系人详细信息和地址信息,在运行时需要使用 JOINS 有效地重新撰写(或反规范化)数据。
SELECT p.FirstName, p.LastName, a.City, cd.Detail
FROM Person p
JOIN ContactDetail cd ON cd.PersonId = p.Id
JOIN ContactDetailType cdt ON cdt.Id = cd.TypeId
JOIN Address a ON a.PersonId = p.Id
更新单个人员的联系人详细信息和地址需要跨多个独立的表格执行写入操作。
现在让我们看看如何在 Azure Cosmos DB 中将同一数据建模为自包含实体。
{
"id": "1",
"firstName": "Thomas",
"lastName": "Andersen",
"addresses": [
{
"line1": "100 Some Street",
"line2": "Unit 1",
"city": "Seattle",
"state": "WA",
"zip": 98012
}
],
"contactDetails": [
{"email": "thomas@andersen.com"},
{"phone": "+1 555 555-5555", "extension": 5555}
]
}
使用此方法,我们已将与此人相关的所有信息(例如其联系人详细信息和地址)嵌入到单个 JSON 文档中,来非规范化人员记录。 此外,由于我们没有被固定的架构所限制,因此我们可以灵活地进行操作,比如拥有完全不同格式的联系方式。
从数据库检索完整的人员记录现在只需对单个容器中的单个项进行一次读取操作。 更新人员记录的联系人详细信息和地址也是针对单个项的“单个写入操作”。
数据反规范化可能会减少您的应用程序完成常见操作所需的查询数和更新。
何时嵌入
通常在下列情况下使用嵌入式数据模型:
- 实体之间存在“包含”关系。
- 实体之间存在“一对多”关系。
- 数据 不常更改。
- 数据 不会在没有绑定的情况下增长。
- 数据经常被一起查询。
注释
通常非规范化数据模型具有更好的读取性能。
何时不嵌入
尽管在 Azure Cosmos DB 中,经验法则是将所有内容非规范化并将所有数据嵌入到单个项中,但这种方法可能会导致需要避免的情况。
以下面的 JSON 代码段为例。
{
"id": "1",
"name": "What's new in the coolest Cloud",
"summary": "A blog post by someone real famous",
"comments": [
{"id": 1, "author": "anon", "comment": "something useful, I'm sure"},
{"id": 2, "author": "bob", "comment": "wisdom from the interwebs"},
…
{"id": 100001, "author": "jane", "comment": "and on we go ..."},
…
{"id": 1000000001, "author": "angry", "comment": "blah angry blah angry"},
…
{"id": ∞ + 1, "author": "bored", "comment": "oh man, will this ever end?"},
]
}
如果我们以典型的博客或内容管理系统(CMS)为模型,那么这个示例可能展示了一个带有嵌入评论的帖子实体。 此示例中的问题是评论数组没有限制,这意味着任何单个发布的评论数都没有(实际)限制。 此设计可能会导致问题,因为项目的大小可以无限大,因此请避免此问题。
随着项大小的增加,大规模传输、读取和更新数据变得更加具有挑战性。
在此情况下,最好是考虑以下数据模型。
Post item:
{
"id": "1",
"name": "What's new in the coolest Cloud",
"summary": "A blog post by someone real famous",
"recentComments": [
{"id": 1, "author": "anon", "comment": "something useful, I'm sure"},
{"id": 2, "author": "bob", "comment": "wisdom from the interwebs"},
{"id": 3, "author": "jane", "comment": "....."}
]
}
Comment items:
[
{"id": 4, "postId": "1", "author": "anon", "comment": "more goodness"},
{"id": 5, "postId": "1", "author": "bob", "comment": "tails from the field"},
...
{"id": 99, "postId": "1", "author": "angry", "comment": "blah angry blah angry"},
{"id": 100, "postId": "2", "author": "anon", "comment": "yet more"},
...
{"id": 199, "postId": "2", "author": "bored", "comment": "will this ever end?"}
]
此模型为每个评论准备了一个条目,并在其中包含一个属性,该属性持有帖子标识符。 此模型允许帖子包含任意数量的评论并高效增长。 如果用户想要查看的内容不止是最近的评论,则需通过传递 postId(应为评论容器的分区键)查询此容器。
另一种嵌入数据不是好主意的情况是,当嵌入的数据在多个项目中频繁使用并且频繁变化时。
以下面的 JSON 代码段为例。
{
"id": "1",
"firstName": "Thomas",
"lastName": "Andersen",
"holdings": [
{
"numberHeld": 100,
"stock": { "symbol": "zbzb", "open": 1, "high": 2, "low": 0.5 }
},
{
"numberHeld": 50,
"stock": { "symbol": "xcxc", "open": 89, "high": 93.24, "low": 88.87 }
}
]
}
此示例可以表示人员的股票组合。 我们选择将股票信息嵌入每个项目组合文档中。 在一个相关数据经常变动的环境中,嵌入这些经常变化的数据就意味着你必须持续更新每个项目组合。 使用股票交易应用程序的示例,每次交易股票时,都会更新每个投资组合项目。
股票 zbzb 可以在一天内交易数百次,成千上万的用户可以在他们的投资组合中交易 zbzb 。 使用类似示例的数据模型,系统每天必须多次更新数千个项目组合文档,这无法很好地扩展。
参考数据
在许多情况下,嵌入数据非常有效,但在某些情况下,数据非规范化会导致问题多于其价值。 那么,你能做什么?
可以在文档数据库中的实体之间创建关系,而不仅仅是在关系数据库中创建关系。 在文档数据库中,一个项目可以包含与其他文档中的数据相关的信息。 Azure Cosmos DB 不是针对复杂关系而设计的,例如关系数据库中的关系,但项之间的简单链接是可能的,并且可能会有所帮助。
在 JSON 中,我们使用前面提供的股票组合示例,但这次我们引用了投资组合中的股票项,而不是嵌入它。 这样,当库存品全天频繁更改时,唯一需要更新的项目是单个库存文档。
Person document:
{
"id": "1",
"firstName": "Thomas",
"lastName": "Andersen",
"holdings": [
{ "numberHeld": 100, "stockId": 1},
{ "numberHeld": 50, "stockId": 2}
]
}
Stock documents:
{
"id": "1",
"symbol": "zbzb",
"open": 1,
"high": 2,
"low": 0.5,
"vol": 11970000,
"mkt-cap": 42000000,
"pe": 5.89
},
{
"id": "2",
"symbol": "xcxc",
"open": 89,
"high": 93.24,
"low": 88.87,
"vol": 2970200,
"mkt-cap": 1005000,
"pe": 75.82
}
此方法的一个缺点是,应用程序必须发出多个数据库请求来获取有关人员投资组合中每个股票的信息。 此设计使写入数据更快,因为更新经常发生。 但是,它使读取或查询数据的速度变慢,这对于此系统来说不太重要。
注释
规范化的数据模型可能需要更多的往返访问服务器。
外键呢?
由于缺乏约束概念(例如外键),数据库不会验证文档中的任何文档间关系;这些链接实际上是“弱链接”。如果要确保引用的数据项实际存在,则需要在您的应用程序中执行此步骤,或者在 Azure Cosmos DB 上使用服务器端触发器或存储过程。
何时引用
通常在下列情况下使用规范化的数据模型:
- 表示“一对多”关系。
- 表示“多对多”关系。
- 相关数据频繁更改。
- 引用的数据可能没有限制。
注释
通常规范化能够提供更好的写入性能。
将关系数据存储在何处?
关系的发展有助于确定在哪个项目中存储引用。
让我们看看对出版商和书籍进行建模的 JSON。
Publisher document:
{
"id": "mspress",
"name": "Microsoft Press",
"books": [ 1, 2, 3, ..., 100, ..., 1000]
}
Book documents:
{"id": "1", "name": "Azure Cosmos DB 101" }
{"id": "2", "name": "Azure Cosmos DB for RDBMS Users" }
{"id": "3", "name": "Taking over the world one JSON doc at a time" }
...
{"id": "100", "name": "Learn about Azure Cosmos DB" }
...
{"id": "1000", "name": "Deep Dive into Azure Cosmos DB" }
如果每个出版商的书籍数量较小且增长有限,则将书籍引用存储在发布者项中可能很有用。 但是,如果每个出版商的书籍数量没有限制,那么此数据模型将产生可变、不断增长的数组,类似于示例中的出版商文档。
切换结构会导致模型表示相同的数据,但避免了大型可变集合。
Publisher document:
{
"id": "mspress",
"name": "Microsoft Press"
}
Book documents:
{"id": "1","name": "Azure Cosmos DB 101", "pub-id": "mspress"}
{"id": "2","name": "Azure Cosmos DB for RDBMS Users", "pub-id": "mspress"}
{"id": "3","name": "Taking over the world one JSON doc at a time", "pub-id": "mspress"}
...
{"id": "100","name": "Learn about Azure Cosmos DB", "pub-id": "mspress"}
...
{"id": "1000","name": "Deep Dive into Azure Cosmos DB", "pub-id": "mspress"}
在此示例中,发布者文档不再包含未绑定的集合。 相反,每个书籍文档都包含对其出版商的引用。
我该如何建模多对多关系?
在关系数据库中,多对多关系通常使用联接表建模。 这些关系只是将其他表中的记录联接在一起。
可能想要使用文档复制相同内容,并生成类似以下示例的数据模型。
Author documents:
{"id": "a1", "name": "Thomas Andersen" }
{"id": "a2", "name": "William Wakefield" }
Book documents:
{"id": "b1", "name": "Azure Cosmos DB 101" }
{"id": "b2", "name": "Azure Cosmos DB for RDBMS Users" }
{"id": "b3", "name": "Taking over the world one JSON doc at a time" }
{"id": "b4", "name": "Learn about Azure Cosmos DB" }
{"id": "b5", "name": "Deep Dive into Azure Cosmos DB" }
Joining documents:
{"authorId": "a1", "bookId": "b1" }
{"authorId": "a2", "bookId": "b1" }
{"authorId": "a1", "bookId": "b2" }
{"authorId": "a1", "bookId": "b3" }
此方法有效,但加载作者及其书籍或书籍及其作者,始终至少需要两个额外的数据库查询。 对联接项进行一个查询,然后用另一个查询来获取实际要联接的项。
如果此联接只是将两个数据片段粘合在一起,那么为什么不将它完全删除? 请看下面的示例。
Author documents:
{"id": "a1", "name": "Thomas Andersen", "books": ["b1", "b2", "b3"]}
{"id": "a2", "name": "William Wakefield", "books": ["b1", "b4"]}
Book documents:
{"id": "b1", "name": "Azure Cosmos DB 101", "authors": ["a1", "a2"]}
{"id": "b2", "name": "Azure Cosmos DB for RDBMS Users", "authors": ["a1"]}
{"id": "b3", "name": "Learn about Azure Cosmos DB", "authors": ["a1"]}
{"id": "b4", "name": "Deep Dive into Azure Cosmos DB", "authors": ["a2"]}
使用此模型,你可以通过查看作者的文档轻松查看作者撰写的书籍。 还可以通过检查书籍文档来查看哪些作者撰写了一本书。 无需使用单独的联接表或进行额外的查询。 通过此模型,应用程序可以更快、更简单地获取所需的数据。
混合数据模型
我们探索嵌入(或非规范化)和引用(或规范化)数据。 每个方法都有好处,并涉及权衡。
它不总是非此即彼。 不要犹豫给事情增添一些变化。
根据应用程序的特定使用模式和工作负载,混合嵌入数据和引用的数据可能有意义。 此方法可以简化应用程序逻辑,减少服务器往返,并保持良好的性能。
请考虑以下 JSON。
Author documents:
{
"id": "a1",
"firstName": "Thomas",
"lastName": "Andersen",
"countOfBooks": 3,
"books": ["b1", "b2", "b3"],
"images": [
{"thumbnail": "https://....png"}
{"profile": "https://....png"}
{"large": "https://....png"}
]
},
{
"id": "a2",
"firstName": "William",
"lastName": "Wakefield",
"countOfBooks": 1,
"books": ["b1"],
"images": [
{"thumbnail": "https://....png"}
]
}
Book documents:
{
"id": "b1",
"name": "Azure Cosmos DB 101",
"authors": [
{"id": "a1", "name": "Thomas Andersen", "thumbnailUrl": "https://....png"},
{"id": "a2", "name": "William Wakefield", "thumbnailUrl": "https://....png"}
]
},
{
"id": "b2",
"name": "Azure Cosmos DB for RDBMS Users",
"authors": [
{"id": "a1", "name": "Thomas Andersen", "thumbnailUrl": "https://....png"},
]
}
在这里,我们(主要是)遵循嵌入模型,将其他实体的数据嵌入到上层文档中,同时引用其他数据。
如果查看书籍文档中的作者数组,会看到一些有趣的字段。 某个 id 字段是用来引用作者文档的字段,这是规范化模型中的标准做法,但是我们还使用了 name 和 thumbnailUrl。 我们可以仅使用 id,并允许应用程序通过“链接”从相应的作者项中检索所需的任何其他信息。然而,由于应用程序会在每本书中显示作者的姓名和缩略图,将一些数据从作者处非规范化可以减少列表中每本书的服务器往返次数。
如果作者的姓名发生更改或更新其照片,则需要更新他们出版的每本书。 但是,对于此应用程序,假设作者很少更改其名称,这种妥协是可接受的设计决策。
在此示例中,有 预先计算的聚合 值,用于在读取操作期间节省处理负担。 在此示例中,嵌入在作者项中的一些数据是在运行时计算的数据。 每次发布新书时,都会创建一个书籍项 ,countOfBooks 字段会根据特定作者存在的书籍文档数设置为计算值。 在读取频繁的系统中,这种优化效果很好,我们可以通过对写操作进行计算来优化读取操作。
因为 Azure Cosmos DB 支持多文档事务,所以构建一个具有预先计算字段的模型是可能的。 由于此限制,许多 NoSQL 存储无法跨文档执行事务,因此提倡设计决策,例如“始终嵌入所有内容”。 在 Azure Cosmos DB 中,可以使用服务器端触发器或存储过程在一个 ACID 事务中插入书籍和更新作者信息等。 现在,无需将所有内容嵌入一个项,只需确保数据保持一致。
区分不同的项类型
在某些情况下,你可能想要在同一集合中混合不同的项类型;如果希望多个相关文档位于同一 分区中,通常会出现此设计选择。 例如,可将书籍和书籍评论放入同一个集合,并按 bookId 将此集合分区。 在这种情况下,通常需要向文档添加一个字段,用于标识其类型以区分它们。
Book documents:
{
"id": "b1",
"name": "Azure Cosmos DB 101",
"bookId": "b1",
"type": "book"
}
Review documents:
{
"id": "r1",
"content": "This book is awesome",
"bookId": "b1",
"type": "review"
}
{
"id": "r2",
"content": "Best book ever!",
"bookId": "b1",
"type": "review"
}
用于 Microsoft Fabric 和 Azure Cosmos DB 镜像的数据建模
Azure Cosmos DB 镜像 是云原生混合事务和分析处理(HTAP)功能,可用于对 Azure Cosmos DB 中的作数据运行近实时分析。 Fabric 镜像功能在 Microsoft Fabric 中实现了 Azure Cosmos DB 与 OneLake 的无缝集成。
通过此集成,可以针对大量数据集运行快速且负担得起的查询。 无需复制数据或担心影响事务工作负荷。 为容器启用镜像时,对数据所做的每一项更改几乎都会立即复制到 OneLake。 无需设置更改源或运行提取、转换和加载 (ETL) 作业。 系统会自动确保两个存储保持同步。
使用 Azure Cosmos DB 镜像功能,现在可以直接从 Microsoft Fabric 连接到 Azure Cosmos DB 容器,并通过 SQL 终结点使用 T-SQL 查询或者直接通过 OneLake 中的 Spark 访问数据,而无需支付请求单位费用。
自动架构推理
Azure Cosmos DB 事务存储是面向行的半结构化数据,而 Microsoft Fabric 中的 OneLake 使用列式和结构化格式。 此转换是为客户自动完成的。 转换过程存在以下方面的限制:最大嵌套级别数目、最大属性数目、不支持的数据类型,等等。
注释
在分析存储的上下文中,我们将以下结构视为属性:
- JSON“元素”或“由
:分隔的字符串-值对” - 由
{和}括起的 JSON 对象 - 由
[和]括起的 JSON 数组
使用以下技术可以充分减轻架构推理转换造成的影响,并充分提高分析能力。
标准化
规范化变得不那么相关,因为 Microsoft Fabric 允许你使用 T-SQL 或 Spark SQL 联接容器。 规范化的预期好处是:
- 较小的数据占用量。
- 较小的交易。
- 减少每个文档的属性数。
- 减少数据结构的嵌套级别数。
数据中属性和层级越少,分析查询速度就越快。 它还有助于确保你的数据的所有部分都包含在 OneLake 中。 OneLake 中表示的级别和属性数有限制。
规范化的另一个重要因素是,OneLake 支持最多包含 1,000 列的结果集,并且公开嵌套列也计入该限制。 换句话说,Fabric 中的 SQL 终结点限制为 1,000 个属性。
但是,既然非规范化是 Azure Cosmos DB 的一项重要数据建模技术,我该怎么做呢? 答案是必须为事务和分析工作负载找到适当的平衡点。
分区键
Microsoft Fabric 中不使用 Azure Cosmos DB 分区键(PK)。 由于这种隔离,你可以为事务数据选择一个主键(PK),专注于数据引入和点读取,同时跨分区查询可以在 Microsoft Fabric 完成。 请看以下示例:
在假设的全局 IoT 方案中,我们将 device id 用作良好的分区键,因为所有设备都会生成类似的数据量,从而防止出现热分区问题。 但是若要分析多个设备的数据,例如“昨天的所有数据”或“每个城市的总计”,你可能会遇到问题,因为查询是一些跨分区查询。 这些查询可能会损害事务性能,因为它们使用一部分吞吐量(以请求单位为单位)来运行。 但是,使用 Microsoft Fabric,无需请求单位成本即可运行这些分析查询。 OneLake 中的增量格式针对分析查询进行优化。
数据类型和属性名称
有关自动架构推理规则的文章列出了支持的数据类型。 虽然Microsoft Fabric 运行时可能以不同的方式处理受支持的数据类型,但不支持的数据类型会阻止分析存储中的表示形式。 例如:使用遵循 ISO 8601 UTC 标准的 DateTime 字符串时,Microsoft Fabric 中的 Spark 池将这些列表示为 string,而 SQL 无服务器则将这些列表示为 varchar(8000)。
数据平展
Azure Cosmos DB 数据顶层的每个属性都会成为分析存储中的列。 嵌套对象或数组中的属性以 JSON 形式存储在 OneLake 中。 嵌套结构需要 Spark 或 SQL 运行时进行额外处理,以展开数据。 处理大量数据时,这可能会增加计算成本和延迟。 在简单的情况下,请考虑对数据使用平面数据模型。 至少,请避免将数据过度嵌套在数据模型中。
该项在 OneLake 中只有两列,id 和 contactDetails。 所有其他数据 email,以及 phone需要通过 SQL 或 Spark 函数进行额外处理才能读取。
{
"id": "1",
"contactDetails": [
{"email": "thomas@andersen.com"},
{"phone": "+1 555 555-5555"}
]
}
平面化消除了这种需求。 以下,id、email和phone都可以作为列直接访问,无需进行额外处理。
{
"id": "1",
"email": "thomas@andersen.com",
"phone": "+1 555 555-5555"
}
数据分层
Microsoft Fabric 允许从以下角度降低成本:
- 在你的事务数据库中运行的查询数量减少。
- 针对数据引入和点读取进行优化的 PK,减少数据占用空间、热分区方案和分区拆分。
- 环境中不会运行任何 ETL 作业,这意味着,无需为此类作业分配请求单位。
受控的冗余
当数据模型已存在且无法更改时,这个方法一种很好的替代方案。 或者,如果数据过于复杂,且嵌套级别过多或属性过多。 如果是这种情况,可以使用 Azure Cosmos DB 更改馈送 将数据复制到另一个容器中,应用所需的转换,然后为该容器配置镜像到 Microsoft Fabric 进行分析。 请看以下示例:
Scenario
容器 CustomersOrdersAndItems 用于存储在线订单,包括客户和项目详细信息:帐单地址、送货地址、送货方法、送货状态、项目价格等。仅表示前 1,000 个属性,并且不包括关键信息,因此无法在 Fabric 中进行分析。 容器具有 PB 级数据,因此无法更改应用程序并重新建模数据。
问题的另一个方面是大量数据。 分析部门经常使用数十亿行,导致他们无法使用 tttl 来删除旧数据。 由于分析需求,维护事务数据库中的整个数据历史记录会强制它们不断增加 RU/秒,从而影响成本。 事务和分析工作负载同时争用相同的资源。
该怎么办?
使用更改源的解决方案
- 解决方案是使用更改馈送来填充三个新容器:
Customers、Orders和Items。 使用更改反馈,可以规范化和展平数据,并从数据模型中删除不必要的信息。 - 容器
CustomersOrdersAndItems现在已将生存时间 (TTL) 设置为仅将数据保留六个月,从而进一步减少了请求单位使用量,因为 Azure Cosmos DB 中每 GB 至少需要 1 个请求单位。 数据更少,请求单位更少。
Takeaways
本文中最大的要点是,无架构方案中的数据建模与以往一样重要。
就像有多种方法可在屏幕上表示一个数据片段一样,数据的建模方法也不会只有一种。 你需要了解应用程序及其生成、使用和处理数据的方式。 通过应用此处介绍的准则,可以创建一个模型,以满足应用程序的即时需求。 应用程序更改时,使用无架构数据库的灵活性轻松调整和改进数据模型。