数据库设计在过去十年中发生了巨大的变化。过去,微调 SQL 查询和数据库索引以确保性能曾经是数据库分析师的工作。如今,开发人员在确保数据的可扩展性方面发挥着更加重要的作用。
曾经单调乏味的数据库设计任务,现在变成了一项需要大量创造力的令人兴奋的任务。在这篇简短的文章中,让我们通过一个现实生活中的问题示例来了解数据库设计是如何发生变化的。
要求
在这个例子中,我们的业务需求是建立一个数据库来存储国家的财产信息。首先,我们需要存储任何财产及其房东。如果房产正在出租,系统还需要存储租户信息。该系统还应记录财产活动,包括购买、出售和租赁。
作为一个典型的数据库系统,用户应该能够通过地址、业主姓名、地区、年龄等任何信息查询房产。系统需要为实时查询和报告目的提供数据服务。
分析
很明显,这里有一些实体,如房东、租户、交易、财产。房东和租户可以进一步分析为扮演不同角色的人。而且,一个人可以出租自己的房子,租另一间房子住,这意味着他可以是一处房产的房东,另一处房产的租客。这给我们留下了三个主要实体:人、财产和交易。人和财产实体彼此之间有很多关系。交易实体链接到一个财产和至少一个人。
如果我们将一些公共属性(如职业、地区和建筑物)分组,则可以引入一些其他有助于减少信息冗余的子实体。
关系数据库 (RDBMS) 时代
如果你是陷入关系数据库时代的开发者之一,那么持久化唯一可行的选择就是关系数据库。自然地,每个实体都将存储在一个表中。如果两个实体之间存在关系,则它们很可能通过外键相互引用。
通过这种设置,冗余为零,每条信息都有单一的真实来源。显然,就存储数据而言,这是最有效的方式。
这里可能存在一个问题,因为实现文本搜索并不容易。无论是十年前还是今天,文本搜索一直没有得到关系数据库很好的支持。 SQL语言本身在语言中提供了一些通配符匹配,但离全文搜索还有很远的距离。
假设您已经完成了定义数据库模式的任务,微调部分通常是数据库分析师的工作;他们将研究每个单独的查询,添加视图、索引,并尝试查询本身以尽可能提高性能。
如果读者在关系数据库上工作了多年,就很容易看出这种方法的局限性。典型的查询可能涉及连接多个表。虽然它适用于少量记录,但当表数增加或每个表中的记录量增加时,解决方案似乎不太可行。数据分片、垂直扩展或添加索引等各种调整只会将性能提高到一定水平。如果我们要处理数亿条记录或连接十多个表,没有什么魔法可以帮助我们。
关系数据库的扩展
为了解决这个问题,开发人员尝试了几种可能比传统关系数据库更好地扩展的技术。这里是其中的一些:
数据库爆炸
这种技术在存储到数据库时反转了规范化数据的过程。例如,我们不是将属性与建筑物、地区或国家表连接起来,而是简单地将相关记录的所有列复制到属性表中。随后,重复和冗余发生。对于建筑物、地区、国家等子实体,不再有单一的事实来源。连接表的步骤被简化了。
爆炸是一个昂贵的过程,可能需要数小时甚至数天才能运行。它牺牲了空间和数据的新鲜度以提高实时查询性能。
添加文档数据库
在这种技术中,关系数据库是真实的来源。然而,为了提供文本搜索,重要的字段被提取并存储在文档数据库中。例如,知道用户会按年龄、性别和姓名搜索人,我们可以创建包含此信息和记录 ID 的文档,并将它们存储到 Solr 或 Elastic Search 服务器。
对系统的实时查询将首先通过在文档数据库中搜索来回答。答案包括一串记录 ID,稍后将由关系数据库用来加载记录。在这种情况下,文档数据库就像一个有助于提供附加功能的外部索引系统。
将整个数据存储到 NoSQL 数据库
另一种选择是将数据存储到 NoSQL 数据库。这种方法可能会为数据维护增加很多复杂性。
为了可视化,我们可以将整个属性或人员对象存储到数据库中。属性对象可以包含所有者和租户作为对象。相反,所有者对象可能包含多个属性对象。在这种情况下,如果数据发生变化,要维护一组相关的文档是一件相当麻烦的事情。
例如,如果一个人购买了房产,我们需要去房产文档更新所有者信息,去那个人的文档更新房产信息。
结合关系数据库和 NoSQL 数据库
现有方法的局限性
浏览完上述方法后,让我们尝试找出每种方法的限制。
- 关系数据库在存储之前对数据进行规范化,以避免重复和冗余。但是,通过优化存储,它会导致检索数据的额外工作量。考虑到数据库通常受查询时间限制,而不是存储,这似乎不是一个好的权衡。
- 爆炸逆转了规范化过程,但它不能提供新数据,因为爆炸通常需要很长时间才能运行。运行爆炸与将整个实体对象存储到面向对象的数据库中相比,后一种选择可能更容易维护。
- 添加一个文档数据库提供了文本搜索,但我觉得它应该颠倒可伸缩性的选项。文档数据库检索速度更快,而关系数据库更适合描述关系。为什么要将文档数据库中的记录 ID 发送回关系数据库以检索记录?如果要查找数百万条记录 ID,可能会发生什么情况?从 NoSQL 数据库中检索这些记录通常比关系数据库更快,因为我们不需要经过连接过程。
- 如上所述,当实体相互关联时,没有简单的方法将它们分离出来存储到面向对象的数据库中。
提议结合关系数据库和 NoSQL 数据库来存储数据
考虑到这些限制,我觉得存储数据的最佳方式可能是结合关系数据库和对象或文档数据库。他们俩都将充当真相的来源,存储他们最擅长的东西。这是我们应该如何拆分数据的解释。
我们以类似于传统关系数据库的方式存储数据,但将列拆分为两种类型的列:
-
存储其他实体 ID(“property_id”、“owner_id”、..)或唯一字段的 ID 或外键的列
-
存储数据的列(“姓名”、“年龄”...)
从数据库架构中删除存储数据的所有列。可以保留一些简单的字段,如“姓名”或“性别”,如果它们有助于通过查看记录为我们提供一些线索。之后,将完整的实体存储到文档数据库中。避免在文档中进行交叉引用。
举例说明方法
让我们通过描述我们应该如何实现一些示例任务来尝试可视化该方法
- 存储用户拥有的新属性
- 将 JPA 配置为仅存储每个主要实体(如人员、财产)的名称和 ID。忽略所有数据字段或子实体,如建筑物、地区、国家/地区。
- 将此属性存储到关系数据库,取回具有持久标识的存储对象。
- 将具有更新 ID 的属性存储到文档数据库,为可搜索字段设置文本索引。
- 将所有者存储到文档数据库但删除到属性的链接。
- 直接查询属性
- 向文档数据库发送查询,检索返回记录。
- 根据所有者信息查询属性
- 向关系数据库发送查询以查找属于所有者的所有属性(假设所有者 ID 已知。如果不知道,则前一步是查询文档数据库以首先找到所有者)。
- 向文档数据库发送查询以通过 ID 查找这些属性。
在上面的步骤中,由于自动生成 ID,我们希望先将记录存储到关系数据库中。在第一步之后,我们有一个非常薄的关系数据库,它只捕获实体之间的关系而不是实体本身。
为了避免交叉引用,我们选择在属性对象中包含所有者信息,而不是在所有者对象中包含属性信息。这是一个实际的选择,取决于预测未来的查询。使用此设置,查询属性将很快,因为它只需要对 noSQL 数据库进行一次查询。
方法总结
最后,让我们总结一下新方法:
- 将主要实体视为独立记录。
- 将子实体视为复杂属性。
- 将主要实体存储到 noSQL 数据库。
- 在关系数据库中存储主要实体的 ID、名称和外键。关系数据库作为桥梁,连接NoSQL数据库中的独立对象。
- 任何 CRUD 总是需要同时提交到两个数据库。
- 由于 ID 生成器,首先将新对象存储到关系数据库中。
优点:
- 从关系数据库中卸载存储数据任务,但让它做它最擅长的事情,存储关系。
- 可以自由选择任何为存储实体提供文本搜索的可扩展数据库。
- NoSQL 数据库的文本搜索和可扩展性以及关系数据库的 RDBMS 搜索两全其美。
缺点:
- 维护两个数据库。
- 没有单一的事实来源。两个数据库之一发生的任何损坏都会导致数据丢失。
- 代码复杂度。
可能的选择
- 将数据存储到提供文本搜索功能的图形数据库。这也很有前途,但我还没有做任何基准来证明可行性。
结论
解决方案非常复杂,但我发现有趣的是,可伸缩性问题是在代码级别而不是数据库级别解决的。通过拆分数据,我们可以解决问题的根本原因,并能够在性能和维护工作之间找到某种平衡。
实现的复杂度很高,但是大数据没有简单的实现。