尽管无架构数据库(如 Azure Cosmos DB)可以轻松存储和查询非结构化和半结构化数据,但考虑数据模型以优化性能、可伸缩性和成本。
如何存储数据? 应用程序如何检索和查询数据? 应用程序是读取密集型还是写入密集型?
阅读本文后,可以回答以下问题:
- 什么是数据建模,我为什么应该关注?
- Azure Cosmos DB 中的数据建模与关系数据库有何不同?
- 如何在非关系数据库中表达数据关系?
- 我应何时嵌入数据和何时链接数据?
JSON 格式的数字
Azure Cosmos DB 将文档保存在 JSON 中,因此在将数字存储在 JSON 中之前,请务必确定是否将数字转换为字符串。 如果数字可能超过String 定义的双精度数字的边界,请将所有数字转换为 a。
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 China 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 China 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 China 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"
}
要点
本文中最大的要点是,无架构方案中的数据建模与以往一样重要。
就像有多种方法可在屏幕上表示一个数据片段一样,数据的建模方法也不会只有一种。 你需要了解应用程序及其生成、使用和处理数据的方式。 通过应用此处介绍的准则,可以创建一个模型,以满足应用程序的即时需求。 应用程序更改时,使用无架构数据库的灵活性轻松调整和改进数据模型。