这是AWS英雄Alex DeBrie的客座帖子。
对于学习Amazon DynamoDB的人来说,单表设计是最令人费解的概念之一。DynamoDB表通常在一个表中包含多个不同的实体,而不是每个实体都有一个表的关系概念。
您可以阅读DynamoDB文档,观看re:Invent演讲或其他视频,或者查看我的书,了解DynamoDB中单表设计的一些设计模式。我想从更高的层次来研究这个主题,特别关注支持和反对单表设计的论点。
在本文中,我们将讨论DynamoDB中的单表设计。我们将从DynamoDB的相关背景开始,这将为数据建模讨论提供信息。然后,我们将讨论单表设计何时对您的应用程序有帮助。最后,我们将以一些在DynamoDB中使用多个表可能更适合您的实例作为结束。
DynamoDB的相关背景
在我们深入讨论单表与多表设计的优点之前,让我们先从DynamoDB的一些背景知识开始。我们没有足够的空间详尽地讨论所有问题,但我想谈谈与单桌与多桌辩论相关的几点。
在我们讨论这些问题时,有一个总的主题将它们联系在一起:DynamoDB希望向您展示现实,以便您能够根据应用程序的需求做出正确的决策。大多数数据库提供对低级位的抽象。这些抽象使您能够更轻松地以灵活的方式查询数据,但它们也对您隐藏了重要的细节。由于这些细节是隐藏的,这些数据库可能会以不可预测的方式扩展,或者随着使用量的增加,很难理解数据库的成本。
考虑到这一点,让我们回顾一下DynamoDB的一些独特功能。
依靠两个核心机制实现一致的扩展
最重要的是,DynamoDB希望在应用程序扩展时提供一致的性能。无论数据库的大小或并发查询的数量如何,DynamoDB都致力于为所有操作提供相同的一位数毫秒响应时间。
为此,DynamoDB依赖于两种核心机制:分区和B树。有了这些坚实的基础,DynamoDB能够将表扩展到数PB的数据和数百万的并发查询。
让我们从分区开始。在传统的关系数据库中,您将所有项存储在单个节点上。随着数据或使用量的增长,您可能会增加实例大小以跟上需求。然而,垂直扩展有其局限性,您经常会发现关系数据库的性能随着数据大小的增加而降低。
为了避免这种情况,DynamoDB使用分区来提供水平可伸缩性。DynamoDB表中的每个项都将包括一个分区键。在后台,DynamoDB将您的数据库分为称为分区的段(如下图1所示),每个段最多保存10GB的数据。

图1:DynamoDB数据库分为三个分区
当请求到达DynamoDB时,请求路由器层查找给定项的分区位置,并将请求路由到适当的分区进行处理,如下面的图2所示。

图2:请求被路由到适当的分区进行处理
随着表的增长,DynamoDB可以无缝地添加新分区并重新分发数据,以适应您的工作负载。元数据子系统保留分区密钥范围到存储节点的映射,并可以快速将请求路由到相关分区。
虽然分区支持水平扩展,但我们通常需要在单个请求中获取一系列相关项。这就是DynamoDB的第二个核心机制。B树是维护排序数据的有效方法。这在许多数据应用程序中都很有用,例如按字母顺序排序用户名或按订单时间戳排序电子商务订单。
DynamoDB将每个分区上的项存储在一个B树中,该B树根据它们的分区键和(如果表使用)排序键排序。该B树为查找密钥提供了对数时间复杂度。在数据子集上使用B树允许对具有相同分区键的项进行高效的范围查询。
通过重点API直接访问数据结构
分区和B树很有趣,但它们并不是DynamoDB所独有的。每个NoSQL数据库都使用某种形式的分区来进行水平扩展,而阳光下的每个数据库都在索引操作中使用B树(或近亲)。
DynamoDB与其他数据库的最大区别在于DynamoDB本机向您公开这些数据结构的方式。没有查询规划器可以将查询解析为一个多步骤过程,从磁盘上的不同位置读取、连接和聚合数据。除了核心分区和B树设置之外,没有灵活的索引策略。
DynamoDB有一个专注的API,让您可以直接访问项目及其基本数据结构。此API分为两大类。对单个项有基本的CRUD操作:PutItem、GetItem、UpdateItem和DeleteItem。这些操作需要完整的主键,您可以认为它们等同于哈希表中的简单查找。
dynamodbapi的第二类包括查询操作,这是一个fetchmany操作,允许您在一个请求中检索多个项。您可以使用此功能获取特定客户的所有订单,或获取物联网传感器的最新读数。
但即使查询操作也会被锁定,因为您必须提供分区密钥的精确匹配,以便将其路由到单个分区来服务请求。它将基于分区键的快速定位与快速搜索和简单的B树顺序读取相结合,以在扩展时提供高效的范围查询。
请注意dynamodbapi没有提供的内容。不能像在关系数据库中那样使用联接操作来组合多个表。也不能使用count、sum、min或max等聚合来压缩大量记录。这些操作是不透明的,并且高度依赖于受查询影响的记录数,而这些记录数无法预先知道。为了在任何规模上提供一致的性能,DynamoDB删除了更高级别的构造,如添加显著可变性的连接和聚合。
基于读取字节和写入字节的透明计费
在上一节中,我们看到DynamoDB通过构建关键数据结构并直接向您公开,使性能对您透明。这样做,它消除了使用不透明查询规划器的数据库的魔力和不可预测性。
DynamoDB在成本方面也做了同样的事情。将数据写入磁盘需要成本。从磁盘读回数据需要花费很多钱。而且这两种成本都随着您读取或写入的数据的大小而增加。DynamoDB通过直接根据您在表中写入和读取的字节向您计费,使您能够清楚地了解这些潜在成本。
DynamoDB的计费基于写容量单位(WCU)和读容量单位(RCU)。快速浏览一下,一个WCU允许您写入1KB的数据,而一个RCU允许您读取4KB的数据。您可以预先提供读写容量单位,也可以使用按需计费来支付每个读写请求。
我喜欢这种透明度。我经常告诉使用DynamoDB的人的一件事是“算算”。如果你粗略估计了你将拥有多少流量,你可以算出它将花费你多少钱。或者,如果您要在两种数据建模方法之间做出选择,您可以通过计算来确定哪种方法更便宜。
正如我们将在下一节中看到的,这种透明的计费模型是在数据建模中使用单表设计原则的一个原因。
为什么以及何时使用单表设计
现在我们已经了解了一些关于DynamoDB的基础知识,让我们看看为什么您可能希望在应用程序中使用单表设计。
在开始之前,我想指出,单表设计的建议适用于单个服务。如果应用程序中有多个服务,则每个服务都应该有自己的DynamoDB表。您应该考虑一个类似于RDBMS实例的DynamoDB表。如果您有一个单独的RDBMS示例,那么您应该有一个独立的DynamoDB表。
此外,如果在何时将实体合并到单个表中有一条经验法则,那就是:一起访问的项应该一起存储。如果您将数据存储在RDBMS中的两个不同的表中,并经常将这两个表连接起来,则应考虑将它们存储在一个非规范化的DynamoDB表中。如果没有,如果你选择的话,你通常可以把它们分开。
使用DynamoDB的单表设计有三个功能性原因,还有一个非功能性的额外原因。现在让我们看看。
使用单表设计在DynamoDB中提供物化连接
在后台部分,我们看到DynamoDB有一个专注的API,它删除了常见的SQL操作,比如连接。
但是联接是有用的!如果我有一对多或多对多的关系,我可能会有一种访问模式,在这种模式中,我获取一个项,但也需要一些有关父项的信息。
当我第一次使用DynamoDB时,我使用了一个类似于关系数据库的多表系统。因为DynamoDB不提供开箱即用的连接,所以我只是在我的应用程序代码中实现了连接。例如,假设我想为特定的访问模式获取客户和客户的订单。为此,我将首先获取客户记录,获取其主键,然后获取具有外键关系的相关订单。

图3:从多表设计中获取信息
但这是一种处理这种用例的低效方法。从我的应用程序到DynamoDB的I/O是我应用程序处理过程中最慢的部分,我通过发出两个独立的顺序查询进行了两次,如上图3所示。
如果我知道这将是我的应用程序中常见的访问模式,我可以依靠DynamoDB的核心数据结构和API来优化它。我可以预先连接相关的项目,并提前实现连接,而不是发出单独的、连续的请求。如果我为客户项目提供与相关订单项目相同的分区键,则它们将位于同一分区上,并根据排序键进行排序。然后,我可以使用查询操作在一个高效的请求中获取所有项,如下面的图4所示。

图4:从单表数据库获取信息
这是使用单表设计的典型示例。我们可以处理涉及异构项的访问模式,同时仍然可以从DynamoDB获得我们期望的一致性能。
使用单表设计降低大型项目的成本
使用单表设计原则的第二个原因是为了降低DynamoDB的成本。
在许多NoSQL系统中,鼓励您创建包含相关嵌套数据的更大的非规范化文档。这是因为您经常将相关数据收集在一起,将数据作为单个记录保存在一起比单独记录更有效。
虽然这个策略可能是一个很好的建议,但要注意不要走得太远。请记住,DynamoDB使成本对您来说清晰易懂,读写成本随着项目的大小而增加。
一个大型项目通常有两组不同的属性:一些大型、移动缓慢的属性与小型、快速移动的属性相结合。例如,想象一个项目代表YouTube上的视频。有很多关于视频本身的数据,例如可用的各种分辨率及其位置、视频描述、字幕和信息卡。这是大量的信息,很少发生变化。
然而,YouTube视频也有一个计数器,显示视频的浏览次数。这是一个很小的属性——只有几位数据,但它可能每天增加数千次。如果将此计数器存储在与视频元数据相同的项目上,则每次要增加视图计数时,可能需要支付多个WCU。此模式如下图5所示。

图5:增加ViewCount属性作为项元数据的一部分
相反,您可以将此项分为两项:视频项和VideoStats项。录制视图时,您将只增加小的VideoStats项。显示视频时,可以使用查询操作获取这两个项目,如下图6所示。

图6:独立于元数据增加ViewCount属性
有了这种模式,我们可以利用DynamoDB的成本透明性和无模式性来优化我们的应用程序需求。
使用单表设计减少您的操作负担
使用单个DynamoDB表的第三个原因是减少您的操作负担。这里的数学很简单:你拥有的东西越少,你需要监控的东西就越少!这里的逻辑有点复杂,特别是考虑到AWS对DynamoDB的改进。
让我们从论点的强版本开始。虽然DynamoDB表与关系数据库中的表有一些共同之处,但也存在一些差异。最重要的是,每个DynamoDB表都是一个独立的基础设施。该基础架构需要配置、监控、报警和备份。如果您的应用程序中有15个不同的实体,因此有15个DynamoDB表,这可能会成为一个负担。
按照逻辑,通过将数据合并到一个表中,可以减少操作负担。您只需要监视一个表,而不是15个。此外,AWS对AWS区域中的表数量加上并发控制平面操作的数量有限制。如果您有一个大客户或正在按客户进行表格分割,您可以达到这些限制。
为多个实体使用一个表甚至可以提高常规表的性能。DynamoDB在分区级别提供突发容量,允许您在尽最大努力的基础上在短时间内超过所提供的吞吐量。如果您有一个更大的表,您的项目将分布在更多的分区上,从而减少潜在的限制爆炸半径。
最后,少数访问模式控制了应用程序的读写能力,这一点通常是正确的。通过将实体组合到一个表中,使用频率较低的访问模式可以融入核心模式的过剩容量。
尽管如此,我认为这一论点是我考虑的一个小因素。DynamoDB表的维护非常简单,并且可以通过AWS CloudFormation或其他基础设施作为代码工具实现自动化。您可以通过时间点恢复自动配置自动缩放、设置警报或启用备份。
此外,DynamoDB做了一些改进,进一步减少了这种争论。2018年,DynamoDB宣布了自适应容量,它将您的供应容量扩展到整个表中最需要的分区。然后,在2019年,DynamoDB宣布了按需计费模式。如果管理您的容量是一项负担,您可以切换到按需模式,只支付您需要的资源。
它迫使您正确思考DynamoDB
作为一个喜欢帮助人们学习DynamoDB并很好地使用它的人,我的最后一个原因是(自私地)关于学习过程,而不是任何特定的应用程序或操作好处。这一论点如下:DynamoDB中对单表设计的强调有助于明确使用DynamoDB进行建模不同于在关系数据库中进行建模的信息。
很多新的DynamoDB用户都像我一样,将他们的关系数据模型提升并转换成一堆DynamoDB表。然后,他们通过在内存中进行连接和聚合,在应用程序中编写一个糟糕版本的查询处理器。这种方法导致应用程序运行缓慢,无法获得DynamoDB提供的可伸缩性和可预测性。
告诉人们大多数服务都可以使用一个表,说明DynamoDB表不能直接与关系数据库表相比较。用户深入挖掘后意识到,他们需要首先关注访问模式,而不是数据的抽象、规范化版本。他们学习了建模NoSQL数据存储以优化性能的关键策略。
在这一点上,我认为学习DynamoDB会使您成为一名更好的开发人员。因为DynamoDB向您展示了基础,所以您了解到您之前使用的一些抽象不是免费的。即使回到关系数据库,您也会更仔细地查看连接和聚合等功能,了解性能配置文件与通过索引字段选择单个记录不同。
在DynamoDB中使用多个表的原因
在上一节中,我们看到了支持DynamoDB中单表设计的核心参数。然而,在一个表和多个表之间的选择是微妙的,在某些情况下,多个表可能是一个不错的选择。让我们在下面探讨其中的一些。
您对DynamoDB流有多种需求
Amazon DynamoDB Streams是我最喜欢的DynamoDB功能之一。我可以获得一个完全管理的变更数据捕获流,其中包括针对我的DynamoDB表的每个写入操作的记录。然后,我可以使用无服务器计算来处理该变化流,以更新聚合、跨系统共享事件或为分析系统提供数据。
DynamoDB流的缺点之一是对并发消费者数量的限制。DynamoDB限制您在一个DynamoDB流上的并发使用者不超过两个。如果您有其他使用者,则处理请求的流将受到限制。
在具有十个或更多实体的单表设计中,超过此限制并不少见。也许新订单项目需要触发AWS Step功能工作流来处理订单,而新客户注册需要通过Amazon EventBridge向其他服务广播注册。在某些情况下,您需要进行一些艰难的权衡,例如向单个流消费者添加更多逻辑,或者连接FIFO SNS主题系统(使用亚马逊简单通知服务(Amazon SNS)和亚马逊简单队列服务(Amazon SQS)队列,以提供事件扇出,同时保留排序语义。
如果是这种情况,将单个表拆分为多个焦点表可能会更容易。每个表可以容纳较少数量的实体,并具有更集中的DynamoDB流管道。
您希望更轻松地导出分析
DynamoDB是一个在线事务处理(OLTP)系统,设计用于对单个记录进行大量并发更新。想象一下常见的面向用户的交互,在讨论线程上下订单或发表评论。它擅长于这些工作负载,并且在处理请求中的少量记录时允许原子操作、低延迟和ACID事务。
相反,DynamoDB并不擅长在线分析处理(OLAP)操作。这些都是内部分析操作。想象一下,一位业务分析师希望了解按类别和地区划分的每周销售额,或者一个营销团队希望找到过去一年中最受欢迎的社交媒体帖子。这些操作不需要高吞吐量或低延迟,但它们确实需要高效地扫描大量数据并执行计算。
为了满足这些OLAP需求,大多数DynamoDB用户将其数据导出到外部分析系统,如Amazon Athena或Amazon Redshift,这些系统专为大规模聚合而构建。然而,将数据从DynamoDB传输到分析系统的机制可能因数据的具体情况而异。
一些用户使用上面讨论的DynamoDB Streams功能将记录流式传输到他们的分析数据库中。通常,这涉及到在加载到Amazon Redshift之前使用Amazon Kinesis Data Firehose在Amazon简单存储服务(Amazon S3)中缓冲数据,或者简单地使用Data Firehoce发送到S3以供Athena查询。这种模式更适用于较大的不可变数据集,因为表的完全导出可能不可行,并且数据的不可变特性适用于OLAP类型的系统。
但有些数据集更小,更易变化,它们有不同的需求。想想应用程序中的用户或客户实体。这些实体在数据仓库中非常重要,可以与其他更大的类似事件的表连接,为事件添加颜色。由于这些实体是可变的,我们希望数据仓库定期更新为当前版本。数据仓库不能很好地处理随机更新,因此通常最好导出一个完整的更新表以加载到系统中。这通常使用红移复制命令或DynamoDB导出到S3操作。
如果在一个DynamoDB表中同时包含这两种类型的数据,这可能会使您的分析需求更加困难。执行完整表导出会比较慢,因为您将导出所有较大的不可变数据集以及较小的可变数据集。通过将不同的数据拆分为不同的表,您可以根据数据的特定形状和需求自定义分析管道。
你不需要这些好处,且这更容易推理
使用多个表的最后一个原因主要是否定了单表设计的情况。
如果上述单表设计的好处对您来说都无关紧要,您没有设置物化连接来获取单个请求中的异类项,也没有将项分解为单独的部分,或者受到操作负担的威胁,那么如果多表设计更容易推理,那么跳过单表设计是可以的。
让我们更深入地了解一下我上面提到的关于在DynamoDB中为连接建模的第一个好处。确实,您希望避免使用依赖于应用程序内连接和对DynamoDB的多个顺序请求的规范化模型。但这并不一定意味着我们必须使用带有物化连接的单表设计。通过构造表以并行而不是顺序获取两组数据,我们可以获得大部分相同的好处。
回想一下上面的例子,我们需要向DynamoDB发出顺序请求,一个请求通过电子邮件地址获取客户记录,另一个请求根据指定的CustomerID获取订单。当切换到单表设计模型时,我们为这两个实体提供了CustomerMailAddress的分区键,这样我们就可以通过单个查询操作获取它们。
此建模开关不需要两个不同的表。如果Orders表使用CustomerMailAddress作为分区键,我们可以在获取客户记录的同时获取Orders记录。
这比发出单个请求稍微慢一点,因为您将等待两个请求中最慢的一个返回。您将支付更多的费用,因为在计算RCU时,您没有获得查询操作的聚合好处。但是,对于客户端在第一页之外获取数据的分页实例,您可能需要实现类似的功能,即使在单表设计中也是如此。如果您和您的应用程序可以接受这些折衷,那么您可以选择多表设计。
请注意,这不是避免学习DynamoDB工作原理的借口!您不应该像关系数据库那样对DynamoDB进行建模,您应该学习DynamoDB数据建模原则。在我多年来所做的几乎所有模型中,如果需要,我们可以将两个单独的表组合成一个表,因为它们首先关注访问模式,然后设计主键来处理这些访问模式。
结论
在这篇文章中,我们学习了使用DynamoDB进行单表设计。首先,我们从DynamoDB的相关背景开始,这对单表讨论非常重要。然后,我们看到了在DynamoDB中使用单表设计的一些原因。最后,我们研究了为什么您可能希望在应用程序中使用多个表的一些原因。
我的最后一个收获是:确保您首先了解使用DynamoDB建模的原则。DynamoDB不是关系数据库,您不应该像使用关系数据库那样使用它。学习曲线感觉很陡峭,但实际上只有三到四个关键概念需要学习,其他一切都是从这条曲线开始的。一旦您了解了这些基础知识,您就能够对应用程序中要使用多少表做出更明智的决定。
如果您想了解DynamoDB数据建模,有很多非常好的资源。我写了《DynamoDB书》,这是一本关于使用DynamoDB进行数据建模的综合指南,它向您介绍了基本概念以及实际应用的示例。我还强烈推荐DynamoDB开发人员文档,因为DynamoDB团队在解释如何正确思考Dynamoda方面做了大量工作。不要害怕潜入并尝试。DynamoDB社区是一个友好的、不断发展的社区,您将在这一过程中获得大量支持。
感谢Joseph Idziorek、Jeff Duffy和Amrith Kumar在我写这篇博客文章时的投入和评论。
关于作者

Alex DeBrie是AWS英雄,也是DynamoDB书籍的作者,该书是使用DynamoDB进行数据建模的综合指南。他是一名独立顾问,与各种规模的公司合作,协助DynamoDB数据建模和无服务器AWS架构实现。在空闲时间,他喜欢运动,并与妻子和四个孩子在一起。在推特上关注他。
原文标题:Single-table vs. multi-table design in Amazon DynamoDB
原文作者:Alex DeBrie
原文链接:https://aws.amazon.com/cn/blogs/database/single-table-vs-multi-table-design-in-amazon-dynamodb/




