Dynamodb中的三个字段组合主键(唯一项)

75

我正在尝试创建一个表来存储DynamoDB中的发票行项目。假设该项目由CompanyCodeInvoiceNumberLineItemId、金额以及其他行项目细节定义。

唯一的项目由前3个属性的组合定义。对于不同的项目,这些属性中的任意2个可以相同。我应该选择什么作为哈希属性和范围属性?


2
你打算如何查询这些记录?你是否总是拥有CompanyCode+InvoiceNumber+LineItemId来进行查询? - James
1
是的,那是其中一个查询,我还需要仅查询CompanyCode。 - HHH
3个回答

76

一些简介

为提高效率,我建议采用完全不同的设计。对于NoSQL数据库(DynamoDB也不例外),我们始终需要先考虑访问模式。此外,如果可能的话,我们应该努力将所有数据放在同一张表格和几个索引中。从OP及其评论中获得的信息,这是两种访问模式:

  1. 对于公司X,获取完整的发票Y(包括所有物品或物品范围)[基于此评论]
  2. 获取公司X的所有发票[基于此评论]

现在我们想知道什么是好的主键?这转化为了问题,什么是好的分区键(PK)和排序键(SK),我们需要创建哪些二级索引以及是何种类型的(本地或全局)?以下是一些提醒:

  • 主键可以是一个列或组合键。
  • 复合主键由分区键和排序键组成。
  • 分区键用作哈希函数的输入,以确定项的分区。
  • 排序键也可以是复合的,这使我们能够将DynamoDB中的一对多关系建模,如评论链接中所示:https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-sort-keys.html
  • 在创建表或索引上的查询时,您始终需要在分区键上使用“=”运算符
  • 当在排序键上查询范围时,您可以使用KeyConditionExpression选项提供的排序运算符集和之间的所有内容(其中之一是函数begins_with(a, substr)
  • 如果需要进一步细化查询结果(筛选投影属性),也可以使用FilterExpression
  • 本地二级索引(LSI)与原始表具有相同的分区键但不同的排序键,并为您提供数据的不同视图,根据替代排序键进行组织
  • 全局二级索引(GSI)具有不同的分区键和不同的排序键,为您提供完全不同的数据视图
  • 所有具有相同分区键的项目都存储在一起,并且对于复合主键,按排序键值排序。如果集合大小大于10 GB,则DynamoDB会按排序键拆分分区。
  • 返回建模

    很明显,我们正在处理需要进行建模并适合于同一张表中的多个实体。为了满足表上唯一的分区键条件,CompanyCode成为自然的分区键 - 所以我会确保它是唯一的。如果不是,则需要问自己如何对第二个访问模式进行建模?

    假设我们已经确定了 CompanyCode 的唯一性,让我们简化并说它以电子邮件的形式出现(或者可能是域名或只是一个代码,但我将使用电子邮件进行演示)。

    • 公司和发票之间的关系总是1:多。
    • 发票和商品之间的关系总是1:多。

    我提出如下图所示的设计:Proposed design in DynamoDB

    • 使用PK和SK作为CompanyCodeInvoiceNumber,可以存储有关该公司的发票的所有属性。
    • 我也可以添加SK为Customer的记录,这使我能够存储有关公司的所有属性。
  • 使用GSI1创建反向查找,其中GSI1PK是我的表SK( InvoiceNumber ),而GSI1SK是我的表PK( CompanyCode )。
  • 我正在使用相同的表来存储具有PK的行项目,即 LineItemId CompanyCode (仍然是唯一的)
  • 对于Item实体项,我的GSI1PK仍然是 InvoiceNumber ,而我的GSI1SK是 LineItemId ,它是表的PK,因此与Invoice实体项相同。
  • 现在支持以下访问模式:

    • 如果我想获取公司X的发票Y及其所有项目(访问模式1):查询表格,其中 CompanyCode = X ,并使用键条件表达式在排序键 InvoiceNumber 上使用 = 运算符。如果我想获取与该发票相关的所有项目,则将ProjectionExpression Items 属性进行投影。
    • 通过检索先前针对公司X和发票Y的所有项目的查询,我现在可以在表上使用 BatchGetItem API调用(使用我的唯一复合键 LineItemId + CompanyCode )获取属于特定客户的特定发票的所有项目。(这带有一些BatchGetItem API的限制)
    • 为支持访问模式2,我将在PK上使用CompanyCode=X查询,并使用KeyConditionExpression对SK进行筛选,使用begins_with(a, substr)函数/操作符以获取给定公司/客户的所有发票,而不是关于该公司的元数据。
    • 此外,通过上述GSI1,针对任何给定的InvoiceNumber,我可以轻松选择属于该特定发票的所有行项目。请记住:全局二级索引中的键值不需要唯一 - 因此在我的GSI1中,我可以轻松地拥有invoice_1 -> (item_1,item_2)和另一个invoice_1 -> (item_1,item_2),但在GSI中两个项之间的区别在SK中(它将与不同的CompanyCode相关联,但出于演示目的,我使用了invoice_1和invoice_2)。

    36

    我认为 @georgeaf99提供的第一个选项 不可行,因为如果您按照这种方式操作,那么CompanyCode必须在表中是唯一的。因此,每个公司只允许一个项目。我认为第二个解决方案是唯一真正可行的方法。

    您可以将CompanyCode用作哈希键,然后所有其他组合成使项唯一的字段(在本例中是InvoiceNumberLineItemId)需要以某种方式组合成一个值(例如连接使用字段分隔符),这将是您的区间键。不幸的是,这有点丑陋,但这就是像DynamoDB这样的NoSQL数据库的性质。但是,它将允许您成功存储具有正确唯一性的记录。当读取记录时,如果您不想将组合字段解析回其各个部分,则必须添加额外的单独字段以获取InvoiceNumberLineItemID

    如果您每家公司的发票数量不是很大,您可以仅通过哈希键进行查询,并在客户端进行筛选。如果您每家公司有大量发票并且需要能够仅查询单个发票的项目,则应在CompanyCode和InvoiceNumber上创建二级索引。

    10

    我相信你已经明白了,主键(散列+范围)最多只能有两个属性。因此,根据你执行的查询类型和数据大小,你可以以不同的方式构建表格。

    (优化了你上述提到的查询类型:仅使用CompanyCode和所有3个属性)

    小/中型数据集的最佳解决方案:

    • 散列键:CompanyCode
    • 仅使用CompanyCode进行查询,然后在其他两个属性上筛选结果

    大型数据集的最佳解决方案:

    • 散列键:CompanyCode
    • 范围键:InvoiceNumber+LineItemId
    • 这样可以只在索引上进行查询,但表格结构相当丑陋

    6
    或许可以使用公司代码加发票号作为哈希值,使用行项目 ID 作为范围,再添加一个针对公司代码的二级索引。这样,您就可以按公司查询,也可以查询特定公司的特定发票。似乎没有必要仅按发票号查询,因为同一发票号可能会用于多家公司。 - jarmod
    2
    赞同jarmod的建议。在应用程序中,使用CompanyCode + InvoiceNumber可以提供更好的基数和最终可扩展性。例如,如果一个公司代码需要为新发票进行大量写入操作,则该哈希键/分区将受到冲击,而通过在表格中分配它可以避免这种情况。 - Raymond Lin
    3
    @jarmod,Dynamodb本身是否有一种方法来处理连接,并且我们只需将这些字段作为单独的字段传递即可?此外,我希望在二级索引中将这些字段作为单独的字段访问,而无需将其再次存储为其他字段的副本。 - HHH
    我也喜欢那个解决方案。如果您不介意将数据存储两次并在更新时发送额外的数据,它可以很好地工作。据我所知,没有办法使一个字段成为两个索引的一部分。 - georgeaf99
    3
    @HHH 我不相信 Dynamo 会为你完成串联操作。如果你想单独查询它们,那么我认为你需要在哈希键外部存储和索引它们。如果你向公司写入了很多发票,这种数据重复就会成为一个问题,但这将是一个非常好的问题;-) - jarmod
    显示剩余2条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接