索引是所有数据库正确数据建模的关键部分, DynamoDB也不例外。DynamoDB 的二级索引是一种强大的工具,可为您的数据启用新的访问模式。
在本博文中,我们将介绍DynamoDB 二级索引。首先,我们将从一些关于如何看待 DynamoDB 以及二级索引解决的问题的概念点开始。然后,我们将介绍一些有效使用二级索引的实用技巧。最后,我们将以一些关于何时应使用二级索引以及何时应寻找其他解决方案的想法作为结束。
让我们开始吧。
什么是 DynamoDB,什么是 DynamoDB 二级索引?
在讨论二级索引的用例和最佳实践之前,我们应该首先了解什么是DynamoDB 二级索引。为此,我们应该了解一些 DynamoDB 的工作原理。
本文假设您对 DynamoDB 有一些基本的了解。我们将介绍您理解二级索引所需了解的基本要点,但如果您是 DynamoDB 新手,您可能需要从更基本的介绍开始。
您需要了解的有关 DynamoDB 的最低要求
DynamoDB 是一种独特的数据库。它专为 OLTP 工作负载而设计,这意味着它非常适合处理大量小型操作 - 比如将商品添加到购物车、喜欢视频或在 Reddit 上添加评论。这样,它可以处理与您可能使用过的其他数据库(如 MySQL、PostgreSQL、 MongoDB或 Cassandra)类似的应用程序。
DynamoDB 的主要承诺是保证在任何规模下都能保持一致的性能。无论您的表包含 1 MB 数据还是 1 PB 数据,DynamoDB 都希望您的 OLTP 类请求具有相同的延迟。这很重要——随着数据量或并发请求数量的增加,许多数据库的性能都会下降。但是,提供这些保证需要一些权衡,并且 DynamoDB 具有一些独特的特性,您需要了解这些特性才能有效地使用它。
首先,DynamoDB 通过在后台将数据分散到多个分区来水平扩展数据库。这些分区对于用户来说是不可见的,但它们是 DynamoDB 工作的核心。您将为表指定一个主键(可以是单个元素,称为“分区键”,也可以是分区键和排序键的组合),DynamoDB 将使用该主键来确定您的数据位于哪个分区。您发出的任何请求都将通过请求路由器,该路由器将确定哪个分区应该处理该请求。这些分区很小 - 通常为 10GB 或更小 - 因此可以移动、拆分、复制和以其他方式独立管理它们。
通过分片实现水平扩展很有趣,但绝不是 DynamoDB 独有的。许多其他数据库(关系型和非关系型)都使用分片进行水平扩展。然而,DynamoDB 的独特之处在于它强制您使用主键来访问数据。DynamoDB 不是使用查询规划器将您的请求转换为一系列查询,而是强制您使用主键来访问数据。您实际上是获得了数据的直接可寻址索引。
DynamoDB 的 API 反映了这一点。有一系列针对单个项目的操作( GetItem
、 PutItem
、 UpdateItem
、 DeleteItem
),允许您读取、写入和删除单个项目。此外,还有一个Query
操作,允许您使用相同的分区键检索多个项目。如果您有一个包含复合主键的表,则具有相同分区键的项目将分组在同一分区上。它们将根据排序键排序,允许您处理诸如“获取用户的最新订单”或“获取 IoT 设备的最后 10 个传感器读数”之类的模式。
例如,假设有一个 SaaS 应用程序,它有一个用户表。所有用户都属于一个组织。我们可能有一张如下所示的表:
我们使用复合主键,分区键为“组织”,排序键为“用户名”。这样,我们就可以通过提供组织和用户名来执行获取或更新单个用户的操作。我们还可以通过仅向Query
操作提供组织来获取单个组织的所有用户。
什么是二级索引?它们如何工作
了解了一些基础知识后,我们现在来看看二级索引。了解二级索引需求的最佳方法是了解它们解决的问题。我们已经了解了 DynamoDB 如何根据主键对数据进行分区,以及它如何促使您使用主键来访问数据。这对于某些访问模式来说都很好,但如果您需要以不同的方式访问数据怎么办?
在上面的示例中,我们有一个用户表,我们通过他们的组织和用户名访问该表。但是,我们可能还需要通过电子邮件地址获取单个用户。这种模式不符合 DynamoDB 推动我们采用的主键访问模式。由于我们的表按不同的属性分区,因此没有明确的方法以我们想要的方式访问我们的数据。我们可以进行全表扫描,但这很慢而且效率低下。我们可以将数据复制到具有不同主键的单独表中,但这会增加复杂性。
这就是二级索引的作用所在。二级索引基本上是具有不同主键的数据的完全托管副本。您可以通过声明索引的主键来指定表上的二级索引。当写入到您的表中时,DynamoDB 会自动将数据复制到您的二级索引。
注意*:本节中的所有内容都适用于全局二级索引。DynamoDB 还提供本地二级索引,但略有不同。在几乎所有情况下,您都需要全局二级索引。有关差异的更多详细信息,请查看有关选择全局或本地二级索引的文章。*
在本例中,我们将向表中添加一个二级索引,其分区键为“电子邮件”。二级索引如下所示:
请注意,这是相同的数据,只是用不同的主键进行了重新组织。现在,我们可以通过电子邮件地址高效地查找用户。
在某些方面,这与其他数据库中的索引非常相似。两者都提供了针对特定属性的查找而优化的数据结构。但 DynamoDB 的二级索引在几个关键方面有所不同。
首先,也是最重要的一点,DynamoDB 的索引位于与主表完全不同的分区上。DynamoDB 希望每次查找都高效且可预测,并且希望提供线性水平扩展。为此,它需要根据您用于查询的属性对数据进行重新分片。
在其他分布式数据库中,它们通常不会为二级索引重新分片数据。它们通常只会为分片上的所有数据维护二级索引。但是,如果您的索引不使用分片键,那么您将失去水平扩展数据的一些好处,因为没有分片键的查询需要在所有分片上执行分散-聚集操作才能找到您要查找的数据。
DynamoDB 二级索引的第二个不同之处在于,它们(通常)将整个项目复制到二级索引。对于关系数据库上的索引,索引通常包含指向被索引项目的主键的指针。在索引中找到相关记录后,数据库将需要获取完整项目。由于 DynamoDB 的二级索引与主表位于不同的节点上,因此它们希望避免通过网络跳转回原始项目。相反,您会将所需的尽可能多的数据复制到二级索引中以处理读取。
DynamoDB 中的二级索引功能强大,但也有一些局限性。首先,它们是只读的——您无法直接写入二级索引。相反,您将写入主表,然后 DynamoDB 将处理到二级索引的复制。其次,您需要为对二级索引的写入操作付费。因此,在表中添加二级索引通常会使表的总写入成本翻倍。
使用二级索引的技巧
现在我们了解了二级索引是什么以及它们如何工作,让我们来谈谈如何有效地使用它们。二级索引是一个强大的工具,但它们可能会被误用。以下是一些有效使用二级索引的技巧。
尝试在二级索引上使用只读模式
第一个技巧似乎很明显——二级索引只能用于读取,因此您应该尽量在二级索引上使用只读模式!然而,我总是看到这个错误。开发人员会先从二级索引读取,然后写入主表。这会导致额外的成本和额外的延迟,而您通常可以通过一些前期规划来避免这种情况。
如果您读过有关 DynamoDB 数据建模的任何内容,您可能知道您应该首先考虑您的访问模式。它不像关系数据库,您首先设计规范化的表,然后编写查询将它们连接在一起。在 DynamoDB 中,您应该考虑应用程序将采取的操作,然后设计表和索引以支持这些操作。
在设计我的表时,我喜欢首先从基于写入的访问模式开始。对于我的写入,我通常会维护某种类型的约束 - 用户名的唯一性或组中的最大成员数。我希望以一种简单明了的方式设计我的表,最好不使用 DynamoDB 事务或使用可能受竞争条件影响的读取-修改-写入模式。
在完成这些工作时,您通常会发现有一种与您的写入模式相匹配的“主要”方法来识别您的项目。这最终将成为您的主键。然后,使用二级索引可以轻松添加额外的次要读取模式。
在我们之前的用户示例中,每个用户请求都可能包含组织和用户名。这将允许我查找单个用户记录以及授权用户执行特定操作。电子邮件地址查找可能针对不太突出的访问模式,例如“忘记密码”流程或“搜索用户”流程。这些是只读模式,它们非常适合二级索引。
当键可变时使用二级索引
使用二级索引的第二个技巧是将它们用于访问模式中的可变值。让我们首先了解其背后的原因,然后看看它适用的情况。
DynamoDB 允许您使用UpdateItem
操作更新现有项目。但是,您无法在更新中更改项目的主键。主键是项目的唯一标识符,更改主键基本上就是创建新项目。如果要更改现有项目的主键,则需要删除旧项目并创建新项目。这个两步过程较慢且成本较高。通常您需要先读取原始项目,然后使用事务删除原始项目并在同一请求中创建新项目。
另一方面,如果您在二级索引的主键中拥有此可变值,则 DynamoDB 将在复制期间为您处理此删除 + 创建过程。您可以发出一个简单的UpdateItem
请求来更改该值,DynamoDB 将处理其余部分。
我发现这种模式主要出现在两种情况下。第一种也是最常见的情况是当您有一个可变属性需要排序时。这里的典型示例是游戏排行榜,玩家不断获得积分,或者是一个不断更新的项目列表,您希望首先显示最近更新的项目。想想 Google Drive 之类的东西,您可以在其中按“上次修改”对文件进行排序。
第二种情况是当您有一个要过滤的可变属性时。在这里,您可以想象一个电子商务商店,里面有用户的订单历史记录。您可能希望允许用户按状态过滤他们的订单——显示我所有“已发货”或“已送达”的订单。您可以将其构建到分区键或排序键的开头,以允许精确匹配过滤。当项目更改状态时,您可以更新状态属性并依靠 DynamoDB 在二级索引中正确分组项目。
在这两种情况下,将此可变属性移至二级索引将为您节省时间和金钱。通过避免读取-修改-写入模式,您可以节省时间,通过避免事务的额外写入成本,您可以节省金钱。
此外,请注意,此模式与上一条提示非常契合。您不太可能根据可变属性(例如其之前的分数、之前的状态或上次更新时间)来识别要写入的项目。相反,您将根据更持久的值(例如用户的 ID、订单 ID 或文件的 ID)进行更新。然后,您将使用二级索引根据可变属性进行排序和筛选。
避免“胖”分区
上文中我们了解到,DynamoDB 根据主键将数据划分为多个分区。DynamoDB 的目标是将这些分区保持在较小的大小(10GB 或更小),并且您应该将请求分散到各个分区,以获得 DynamoDB 可扩展性的优势。
这通常意味着您应该在分区键中使用高基数值。想象一下用户名、订单 ID 或传感器 ID。这些属性有大量值,而 DynamoDB 可以将流量分散到您的分区中。
我经常看到人们在主表中理解这个原则,但在二级索引中却完全忘记了它。通常,他们希望对某一类型的项目在整个表中进行排序。如果他们想按字母顺序检索用户,他们会使用二级索引,其中所有用户都以USERS
作为分区键,以用户名作为排序键。或者,如果他们想对电子商务商店中最近的订单进行排序,他们会使用二级索引,其中所有订单都以ORDERS
作为分区键,以时间戳作为排序键。
此模式适用于流量较小的应用程序,在这些应用程序中,您不会接近DynamoDB 分区吞吐量限制,但对于流量较大的应用程序来说,这是一种危险的模式。您的所有流量可能会集中到单个物理分区,并且您可能会很快达到该分区的写入吞吐量限制。
此外,最危险的是,这可能会给您的主表带来问题。如果您的二级索引在复制期间受到写入限制,则复制队列将备份。如果此队列备份过多,DynamoDB 将开始拒绝对主表的写入。
这是为了帮助您而设计的——DynamoDB 希望限制您的二级索引的陈旧性,因此它将阻止您使用具有大量滞后的二级索引。然而,这可能是一个令人惊讶的情况,在您最意想不到的时候突然出现。
使用稀疏索引作为全局过滤器
人们通常认为二级索引是一种使用新主键复制所有数据的方法。但是,您不需要将所有数据都放在二级索引中。如果您有与索引的键架构不匹配的项目,则不会将其复制到索引中。
这对于提供全局数据过滤器非常有用。我使用的典型示例是消息收件箱。在主表中,您可以按创建时间排序存储特定用户的所有消息。
但如果你和我一样,收件箱里会有很多邮件。此外,你可能会将未读邮件视为“待办事项”列表,就像回复某人的小提醒一样。因此,我通常只想查看收件箱中的未读邮件。
您可以使用二级索引来提供此全局过滤器,其中unread == true
。您的二级索引分区键可能类似于${userId}#UNREAD
,排序键是消息的时间戳。当您最初创建消息时,它将包含二级索引分区键值,因此将被复制到未读消息二级索引。稍后,当用户阅读该消息时,您可以将status
更改为READ
并删除二级索引分区键值。然后 DynamoDB 会将其从您的二级索引中删除。
我一直在使用这个技巧,它非常有效。此外,稀疏索引可以为您节省资金。对已读消息的任何更新都不会复制到辅助索引,这样您就可以节省写入成本。
缩小二级索引投影以减少索引大小和/或写入
作为我们的最后一条技巧,让我们进一步探讨上一点。我们刚刚看到,如果项目没有索引的主键元素,DynamoDB 将不会在二级索引中包含该项目。此技巧不仅可以用于主键元素,还可以用于数据中的非键属性!
创建二级索引时,您可以指定要将主表中的哪些属性包含在二级索引中。这称为索引的投影。您可以选择包含主表中的所有属性、仅包含主键属性或属性的子集。
虽然将所有属性都包含在二级索引中很诱人,但这可能是一个代价高昂的错误。请记住,每次对主表的写入(如果更改了投影属性的值)都将复制到二级索引中。具有完整投影的单个二级索引实际上会使表的写入成本翻倍。每个额外的二级索引都会使写入成本增加1/N + 1
,其中N
是新索引之前的二级索引数量。
此外,您的写入费用是根据项目的大小计算的。每向表中写入 1KB 数据都会使用一个 WCU。如果您将 4KB 项目复制到二级索引,则您将在主表和二级索引上支付全部 4 个 WCU。
因此,您可以通过两种方式缩小二级索引投影来节省资金。首先,您可以完全避免某些写入。如果您的更新操作不会触及二级索引投影中的任何属性,DynamoDB 将跳过对二级索引的写入。其次,对于那些确实复制到二级索引的写入,您可以通过减少复制项目的大小来节省资金。
这可能是一个棘手的平衡问题。创建索引后,二级索引投影不可更改。如果您发现二级索引中需要其他属性,则需要使用新投影创建新索引,然后删除旧索引。
您应该使用二级索引吗?
现在我们已经探讨了有关二级索引的一些实用建议,让我们退一步并提出一个更基本的问题——您是否应该使用二级索引?
正如我们所见,二级索引可帮助您以不同的方式访问数据。然而,这是以额外的写入为代价的。因此,我对二级索引的经验法则是:
当减少的读取成本超过增加的写入成本时,使用二级索引。
这句话说出来似乎很明显,但在建模时却可能违反直觉。说“把它放到辅助索引中”似乎很容易,而没有考虑其他方法。
为了说明这一点,让我们看一下二级索引可能没有意义的两种情况。
小项目集合中有很多可过滤的属性
使用 DynamoDB,您通常希望主键为您进行过滤。每当我在 DynamoDB 中使用查询,然后在应用程序中执行自己的过滤时,我都会感到有点恼火——为什么我不能把它构建到主键中呢?
尽管我内心有这种反应,但在某些情况下您可能希望过度读取数据,然后在应用程序中进行过滤。
最常见的情况是,当您想为用户提供许多不同的数据过滤器,但相关数据集是有限的。
想象一下健身追踪器。您可能希望允许用户根据许多属性进行过滤,例如锻炼类型、强度、持续时间、日期等。但是,用户的锻炼次数是可控的——即使是高级用户也需要一段时间才能超过 1000 次锻炼。您无需对所有这些属性都添加索引,只需获取所有用户的锻炼,然后在您的应用程序中进行过滤即可。
这是我建议进行计算的地方。 DynamoDB 可以轻松计算这两个选项,并了解哪一个更适合您的应用程序。
大型项目集合中有很多可过滤的属性
让我们稍微改变一下情况——如果我们的项目集合很大怎么办?如果我们正在为健身房构建一个锻炼追踪器,并且我们想允许健身房老板根据我们上面提到的所有属性来过滤健身房中的所有用户怎么办?
这改变了情况。现在我们谈论的是数百甚至数千名用户,每个用户都有数百或数千种锻炼方式。过度阅读整个项目集合并对结果进行事后过滤是没有意义的。
但二级索引在这里也没什么意义。二级索引适用于已知的访问模式,您可以依靠相关的过滤器。如果我们希望健身房老板能够根据各种属性进行过滤,而这些属性都是可选的,那么我们需要创建大量索引才能实现这一点。
我们之前讨论过查询规划器可能存在的缺点,但查询规划器也有优点。除了允许更灵活的查询之外,它们还可以执行索引交集等操作,以便在编写这些查询时查看来自多个索引的部分结果。您可以使用 DynamoDB 执行相同的操作,但这会导致您的应用程序反复操作,并且需要一些复杂的应用程序逻辑来弄清楚。
当我遇到这些类型的问题时,我通常会寻找更适合此用例的工具。Rockset 和Elasticsearch是我在此的首选建议,它们可为您的数据集提供灵活的、类似二级索引的过滤。
结论
在本文中,我们了解了 DynamoDB 二级索引。首先,我们了解了一些概念,以了解 DynamoDB 的工作原理以及为什么需要二级索引。然后,我们回顾了一些实用技巧,以了解如何有效地使用二级索引并了解它们的具体特点。最后,我们研究了如何考虑二级索引,以了解何时应该使用其他方法。
二级索引是 DynamoDB 工具箱中的强大工具,但并非万能良药。与所有 DynamoDB 数据建模一样,在开始之前,请务必仔细考虑您的访问模式并计算成本。
在 Alex DeBrie 的博客使用 Rockset 上的 SQL 进行 DynamoDB 过滤和聚合查询中了解有关如何使用 Rockset 进行类似二级索引过滤的更多信息。
转载请注明:可思数据 » 何时使用 DynamoDB 二级索引