

# DynamoDB 表的数据建模
<a name="data-modeling"></a>

在我们深入研究数据建模之前，了解一些 DynamoDB 基础知识很重要。DynamoDB 是一个键/值 NoSQL 数据库，允许灵活的架构。除了每个项目的关键属性外，数据属性集可以是统一的，也可以是离散的。DynamoDB 键架构要么采用简单主键的形式（其中分区键可以唯一地标识项目），要么是采用复合主键的形式（其中分区键和排序键的组合可以唯一地定义项目）。对分区键进行哈希处理，以确定数据的物理位置并检索数据。因此，务必选择高基数和水平可扩展的属性作为分区键，以确保数据的均匀分布。排序键属性在键架构中是可选的，使用排序键可以针对一对多关系建模并在 DynamoDB 中创建项目集合。排序键也称为范围键，用于对项目集合中的项目进行排序，还允许灵活的基于范围的操作。

有关 DynamoDB 键架构的更多详细信息和最佳实践，您可以参考以下内容：
+ [DynamoDB 中的分区和数据分布](HowItWorks.Partitions.md) 
+ [在 DynamoDB 中设计并有效使用分区键的最佳实践](bp-partition-key-design.md) 
+ [在 DynamoDB 中使用排序键整理数据的最佳实践](bp-sort-keys.md) 
+ [选择正确的 DynamoDB 分区键](https://aws.amazon.com/blogs/database/choosing-the-right-dynamodb-partition-key/) 

在 DynamoDB 中，通常需要二级索引来支持其他查询模式。二级索引是影子表，与基表相比，相同的数据通过不同的键架构进行组织。本地二级索引（LSI）与基表共享相同的分区键，并允许使用备用排序键以允许它共享基表的容量。全局二级索引（GSI）可以具有与基表不同的分区键以及不同的排序键属性，这意味着 GSI 的吞吐量管理独立于基表。

有关二级索引和最佳实践的更多详细信息，可以参考以下内容：
+ [在 DynamoDB 中使用二级索引改进数据访问](SecondaryIndexes.md) 
+ [在 DynamoDB 中使用二级索引的最佳实践](bp-indexes.md) 

现在让我们更深入地了解数据建模。在 DynamoDB 或任何相关的 NoSQL 数据库上，设计灵活且高度优化的架构，这个过程所需的技能学习起来并不简单。本模块的目标是协助您开发用于设计架构的思维流程图，让您从使用场景转入生产。首先，我们将介绍在进行所有设计（即单表设计与多表设计）时的基础选择。接下来，我们将查看多种设计模式（构建基块），您可以利用这些模式来为应用程序实现不同的组织或性能结果。最后，我们还为不同的使用场景和行业提供了各种完整的架构设计包。

![\[图中显示了数据、位于数据下方的数据块以及位于数据块下方的基础之间的概念关系。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SchemaDesign.png)


**Topics**
+ [项目集合 - 如何在 DynamoDB 中对一对多关系建模](WorkingWithItemCollections.md)
+ [DynamoDB 中的数据建模基础](data-modeling-foundations.md)
+ [DynamoDB 中的数据建模构建基块](data-modeling-blocks.md)
+ [DynamoDB 中的数据建模架构设计包](data-modeling-schemas.md)
+ [在 DynamoDB 中建模关系数据的最佳实践](bp-relational-modeling.md)

# 项目集合 - 如何在 DynamoDB 中对一对多关系建模
<a name="WorkingWithItemCollections"></a>

在 DynamoDB 中，*项目集合* 是共享相同分区键值的一组项目，这意味着项目是相关的。项目集合是在 DynamoDB 中对一对多关系建模的主要机制。项目集合只能存在于配置为使用[复合主键](HowItWorks.CoreComponents.md#HowItWorks.CoreComponents.PrimaryKey)的表和索引上。

**注意**  
项目集合可以存在于基表或二级索引中。有关项目集合如何与索引交互的更多信息，请参阅[本地二级索引中的项目集合](LSI.md#LSI.ItemCollections)。

考虑下表，其中显示三个不同的用户及其游戏内清单：

![\[三个具有不同属性的不同项目系列。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/item_collection.png)


对于每个集合中的某些项目，排序键是由用于对数据进行分组的信息组成的联接，例如 `inventory::armor`、`inventory::weapon` 或 `info`。每个项目集合可以将这些属性的不同组合作为排序键。用户 `account1234` 有 `inventory::weapons` 项目，而用户 `account1387` 没有（因为他们还没找到任何项目）。用户 `account1138` 只使用两个项目作为他们的排序键（因为他们还没有清单），而其他用户则使用三个项目。

DynamoDB 允许您有选择性地从这些项目集合中检索项目，以执行以下操作：
+ 从特定用户检索所有项目
+ 从特定用户只检索一个项目
+ 检索属于特定用户的特定类型的所有项目

## 通过使用项目集合组织数据来加快查询
<a name="WorkingWithItemCollections.Example"></a>

在此示例中，这三个项目集合中的每个项目都代表一位玩家和我们根据游戏和玩家的访问模式选择的数据模型。游戏需要什么数据？ 游戏什么时候需要数据？ 游戏需要数据的频率是多久？ 这样做的成本是多少？ 这些数据建模决策是根据对这些问题的答案作出的。

在这个游戏中，向玩家呈现了不同的武器清单页面和另一个装甲页面。当玩家打开其清单时，首先显示武器，因为我们希望该页面加载速度非常快，而后续的清单页面可以在其后加载。随着玩家获得更多游戏内项目，这些项目类型中的每种类型都可能相当大。因此，我们决定每个清单页面都是玩家在数据库中的项目集合中的自己的项目。

以下部分将详细介绍如何通过 `Query` 操作与项目集合交互。

**Topics**
+ [通过使用项目集合组织数据来加快查询](#WorkingWithItemCollections.Example)

# DynamoDB 中的数据建模基础
<a name="data-modeling-foundations"></a>

本节介绍基础层，首先探讨两种类型的表设计：单表和多表。

![\[图中显示了数据、位于数据下方的数据块以及位于数据块下方的基础之间的概念关系。重点强调基础。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SchemaDesignFoundation.png)


## 单表设计基础
<a name="data-modeling-foundations-single"></a>

对于 DynamoDB 架构的基础，一个选择是**单表设计**。单表设计模式让您可以在单个 DynamoDB 表中存储多种类型（实体）的数据。这种模式消除了维护多个表及其之间的复杂关系的需求，从而优化数据访问模式、提升性能并降低成本。能够做到这一点，是因为 DynamoDB 将具有相同分区键的项目（称为项目集合）分别存储在相同的分区上。在这种设计中，不同类型的数据作为项目存储在同一个表中，每个项目由唯一的排序键标识。

![\[图中显示了一张表，以及在同一个 UserID 项目集合中，如何使用排序键按实体类型区分每个项目。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SingleTableSchema.png)


**优势**：
+ 数据局部性，支持在一个数据库调用中查询多种实体类型
+ 降低读取操作的整体费用成本和延迟成本：
  + 采用最终一致性模式，对两个大小总计小于 4KB 的项目执行单次查询的成本为 0.5 RCU
  + 采用最终一致性模式，对两个大小总计小于 4KB 的项目执行两次查询的成本为 1 RCU（每次 0.5 RCU）
  + 返回两次单独的数据库调用的时间平均要长于一次调用的时间
+ 减少要管理的表的数量：
  + 无需在多个 IAM 角色或 IAM 策略之间维护权限 
  + 表的容量管理在所有实体之间平均分布，这通常可以让使用模式更好预测
  + 进行监控所需的警报更少
  + 只需在一个表上轮换客户托管加密密钥
+ 让流向表的流量变得平滑：
  + 通过将多种使用模式聚合到同一个表，总体使用情况会趋向于更平滑（就如同股票指数的表现往往比任何单个股票都更平滑），这有助于在预置模式表中可以更好地实现更高的利用率

**缺点**
+ 与关系数据库相比，由于矛盾的设计，学习曲线可能很陡峭
+ 所有实体类型的数据要求必须一致
  + 要么全部备份，要么都不备份，因此如果某些数据无关紧要，请考虑将其放在单独的表中
  + 所有项目共享表加密。对于具有单个租户加密要求的多租户应用程序，需要进行客户端加密
  + 对于混合了历史数据和运营数据的表而言，在启用不频繁访问存储类时不会获得太多益处。有关更多信息，请参阅 [DynamoDB 表类](HowItWorks.TableClasses.md)。
+ 即使只需要处理一部分实体，也会将所有更改的数据都传播到 DynamoDB Streams。
  + 得益于 Lambda 事件筛选条件，在使用 Lambda 时这不会影响您的账单，但在使用 Kinesis Consumer Library 时会增加成本 
+ 使用 GraphQL 时，单表设计将更难实施
+ 使用更高级别的 SDK 客户端（如 Java [`DynamoDBMapper`](DynamoDBMapper.md) 或 [增强型客户端](DynamoDBEnhanced.md)）时，处理结果可能更加困难，因为同一个响应中的项目可能与不同的类相关联

**何时使用**：

对于那些经常同时查询多个实体类型或需要维护不同数据类型之间关系的应用程序，单表设计非常适合。当访问模式受益于数据局部性时，以及当您希望最大限度地减少管理多个表的开销时，单表设计尤其有效。

## 多表设计基础
<a name="data-modeling-foundations-multi"></a>

对于 DynamoDB 架构的基础，第二个选择是多表设计****。多表设计模式更像是传统数据库设计，在这种模式下，您在每个 DynamoDB 表中存储单一类型（实体）的数据。每个表中的数据仍按分区键组织，因此可以针对可扩展性和性能对单个实体类型的性能进行优化，但是跨多个表的查询必须独立完成。

![\[图中显示了论坛表，其中包含论坛的列表和一些聚合数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/MultipleTable1.png)


![\[图中显示了话题表，其中包含按话题所属的特定论坛划分的话题列表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/MultipleTable2.png)


**优点**
+ 对于那些不习惯使用单表设计的用户而言，此模式设计起来更简单 
+ 由于每个解析器都映射到单个实体（表），因此更容易实施 GraphQL 解析器
+ 允许跨不同实体类型实现独特数据要求：
  + 可以对单独的任务关键型表进行备份 
  + 可以分别管理各个表的表加密。对于具有单独租户加密要求的多租户应用程序，通过使用单独的租户表，每个客户都可以拥有自己的加密密钥
  + 可以仅在存储历史数据的表上启用不频繁访问存储类，从而实现充分的成本节约益处。有关更多信息，请参阅 [DynamoDB 表类](HowItWorks.TableClasses.md)。
+ 每个表都有自己的更改数据流，这样便可以为每种类型的项目设计专用的 Lambda 函数，而不是采用单个的整体式处理程序

**缺点**
+ 对于需要多个表中数据的访问模式，将需要从 DynamoDB 进行多次读取，并且可能需要通过客户端代码来处理/联接数据。
+ 多个表的操作和监控需要更多 CloudWatch 警报，并且每个表必须独立扩展
+ 每个表的权限都需要单独管理。以后添加表时需要更改任何必要的 IAM 角色或 IAM 策略

**何时使用**：

如果您的应用程序的访问模式不需要同时查询多个实体或表，那么多表设计是一种不错且足够的方法。

# DynamoDB 中的数据建模构建基块
<a name="data-modeling-blocks"></a>

本节介绍构建基块层，它为您提供可以在应用程序中使用的设计模式。

![\[图中显示了数据、位于数据下方的数据块以及位于数据块下方的基础之间的概念关系。重点强调基础。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SchemaDesignBlocks.png)


**Topics**
+ [复合排序键构建基块](#data-modeling-blocks-composite)
+ [多租户构建基块](#data-modeling-blocks-multi-tenancy)
+ [稀疏索引构建基块](#data-modeling-blocks-sparse-index)
+ [生存时间构建基块](#data-modeling-blocks-ttl)
+ [存档生存时间构建基块](#data-modeling-blocks-ttl-archival)
+ [垂直分区构建基块](#data-modeling-blocks-vertical-partitioning)
+ [写入分片构建基块](#data-modeling-blocks-write-sharding)

## 复合排序键构建基块
<a name="data-modeling-blocks-composite"></a>

用户可能会认为 NoSQL 也是非关系型的。归根结底，没有理由不能在 DynamoDB 架构中放入关系，它们只是看起来与关系数据库及其外键不同。在 DynamoDB 中，我们可以用来建立数据的逻辑层次结构的最关键模式之一是复合排序键。在设计时，最常见样式使用 \$1 分隔层次结构的每一层（父层 > 子层 > 孙子层）。例如 `PARENT#CHILD#GRANDCHILD#ETC`。

![\[图中显示了表中的一项，该项以 userID 作为主键，以其他属性的组合作为排序键。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ShoppingCart.png)


虽然在 DynamoDB 中，分区键总是需要确切的值才能查询数据，但我们可以对排序键从左到右应用部分条件，类似于遍历二叉树。

在上面的示例中，我们有一家电子商务网店，提供了需要在用户的不同会话之间维护的购物车。每当用户登录时，他们需要能够查看完整的购物车，包括保存起以便将来购买的商品。但是，当他们进入结账环节时，只能加载活动购物车中的商品进行购买。由于这些 `KeyConditions` 都明确要求提供 CART 排序键，因此 DynamoDB 在读取时会直接忽略其他心愿单中的数据。虽然已保存的商品和活动商品都放在购物车中，但在应用程序的不同部分中，我们需要以不同的方式对待它们，因此，要想仅检索应用程序的各个部分所需的数据，最优方法是对排序键的前缀应用 `KeyCondition`。

**此构建基块的主要特点**
+ 相关项目存储在本地同一相对位置，以实现高效的数据访问 
+ 使用 `KeyCondition` 表达式，可以有选择地检索层次结构的子集，这意味着不会浪费 RCU 
+ 应用程序的不同部分可以将其项目存储在特定前缀下，以防止项目被覆盖或写入冲突

## 多租户构建基块
<a name="data-modeling-blocks-multi-tenancy"></a>

许多客户使用 DynamoDB 托管其多租户应用程序的数据。对于这些场景，我们希望设计一种架构，将单个租户的所有数据保留在表的该租户自己的逻辑分区中。这利用了项目集合的概念，该术语指的是 DynamoDB 表中具有相同分区键的所有项目。有关 DynamoDB 如何实现多租户的更多信息，请参阅 [Multitenancy on DynamoDB](https://docs.aws.amazon.com/whitepapers/latest/multi-tenant-saas-storage-strategies/multitenancy-on-dynamodb.html)。

![\[图中显示了一张可用于代表多租户照片网站的表。主键的构成是将用户作为分区键，将不同的照片作为排序键。每个项目的属性显示了托管照片位置的 URL。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/MultiTenant.png)


在本示例中，我们运行的是一个照片托管网站，用户数可能会成千上万。每个用户最初只能将照片上传到自己的个人资料中，而且在默认情况下，我们不允许用户查看任何其他用户的照片。理想情况下，在每个用户调用您 API 的授权中，应该添加额外的隔离级别，以确保他们只能请求自己分区中的数据，但是在架构级别，唯一的分区键就足够了。

**此构建基块的主要特点**
+ 任何一位用户或租户读取的数据量只能等于其自身分区中的项目总量
+ 在账户关闭或由于合规性要求而要删除租户数据时，可以巧妙地完成删除，而且成本很低。只需运行分区键等于其租户 ID 的查询，然后对返回的每个主键执行 `DeleteItem` 操作

**注意**  
通过在设计时考虑多租户的情况，您便可在单个表中，使用不同的加密密钥提供程序来安全地隔离数据。适用于 Amazon DynamoDB 的 [AWS 数据库加密 SDK](https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/what-is-database-encryption-sdk.html) 让您可以在 DynamoDB 工作负载中提供客户端加密功能。您可以执行属性级别的加密，这样就可以先对特定属性值进行加密，然后再存储到 DynamoDB 表中，而且无需预先解密整个数据库即可搜索加密的属性。

## 稀疏索引构建基块
<a name="data-modeling-blocks-sparse-index"></a>

有时，访问模式需要查找与稀少项目或接收状态的项目（这需要上报响应）相匹配的项目。我们无需频繁地在整个数据集中查询这些项目，而是可以随数据一起稀疏加载**全局二级索引 (GSI)**。这意味着基表中的项目，只有在索引中定义了属性时才会复制到索引中。

![\[图中显示了一个接收大量稳定状态数据的基表\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SparseBaseTable.png)


![\[图中显示了一个全局二级索引，该索引仅接收已上报的项目\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SparseGSI.png)


在此示例中，我们看到了一个 IOT 使用场景，在该场景中，现场的每台设备都会定期报告状态。我们预计在绝大多数报告中，设备会报告一切正常，但有时可能会出现故障，这时必须上报到维修技术人员。对于带有上报情况的报告，属性 `EscalatedTo` 会添加到项目中，其他情况下不会有此属性。本示例中的 GSI 按 `EscalatedTo` 分区，由于 GSI 从基表中引入了键，我们仍然可以查看报告了故障的 DeviceID 以及报告故障的时间。

虽然在 DynamoDB 中读取比写入的成本更低，但稀疏索引是一种非常强大的工具，适用于特定类型项目的实例很少，但进行读取以查找这些实例却很常见的使用场景。

**此构建基块的主要特点**
+ 稀疏 GSI 的写入和存储成本仅应用到与键模式匹配的项目，因此稀疏 GSI 的成本可能大大低于将所有项目复制到其中的其他 GSI 的成本 
+ 复合排序键仍可用于进一步缩小与所需查询匹配的项目的范围，例如，时间戳可用作排序键，以便仅查看最近 X 分钟 (`SK > 5 minutes ago, ScanIndexForward: False`) 内报告的错误

## 生存时间构建基块
<a name="data-modeling-blocks-ttl"></a>

大多数数据在一定的持续时间内，可以认为值得将其保存在主数据存储中。为了协助对 DynamoDB 中的数据进行老化处理，它具有一项名为**生存时间（TTL）**的功能。利用 [TTL](TTL.md) 功能，您可以为需要监控的属性，在表级别定义带有纪元时间戳（过去的时间）的特定属性。这让您可以免费从表中删除过期的记录。

**注意**  
如果使用全局表的[全局表版本 2019.11.21（当前版）](GlobalTables.md)，并且还使用[生存时间](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html)特征，则 DynamoDB 会将 TTL 删除复制到所有副本表。在出现 TTL 到期的区域中，初始 TTL 删除不会消耗写入容量。但是，在每个副本区域中，复制到副本表的 TTL 删除将消耗复制的写容量单位，并且将收取适用的费用。

![\[图中显示的表的用户消息具有生存时间属性\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/TTL.png)


在此示例中，我们有一个应用程序，设计用于让用户创建短暂存在的消息。在 DynamoDB 中创建消息时，应用程序代码会将 TTL 属性设置为 7 天以后的日期。大约 7 天后，DynamoDB 会发现这些项目的纪元时间戳为过去的时间，并且会删除这些项目。

由于按 TTL 执行删除是免费的，因此强烈建议使用此功能从表中删除历史数据。这可以减少每个月的总存储账单费用，并且有可能减少用户的读取成本，因为这减少了他们在查询时需要检索的数据量。虽然可以在表级别启用 TTL，但您需要确定为哪些项目或实体创建 TTL 属性，以及将纪元时间戳设置为未来多长时间。

**此构建基块的主要特点**
+ TTL 删除操作在后台运行，不会影响您的表性能 
+ TTL 是一个异步进程，大约每六小时运行一次，但最多可能需要 48 小时才能删除过期的记录 
  + 对于锁定记录或者状态管理等使用场景，如果必须在 48 小时内清理过时数据，则不要依赖 TTL 删除操作 
+ 您可以将 TTL 属性命名为有效的属性名称，但该值必须是数值类型

## 存档生存时间构建基块
<a name="data-modeling-blocks-ttl-archival"></a>

尽管 TTL 是从 DynamoDB 中删除较早数据的有效工具，但在许多使用场景中，会要求将数据存档保留比主数据存储更长的时间。在这种情况下，我们可以利用 TTL 的定时删除记录，将过期记录推送到长期数据存储中。

![\[图中显示了一个表，该表将生存时间删除作业发送到 DynamoDB Streams 中，然后发送到长期数据存储。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/TTLArchive.png)


当 DynamoDB 完成 TTL 删除时，该操作仍会作为 `Delete` 事件推送到 DynamoDB Streams 中。但是，当 DynamoDB TTL 是执行删除操作的一方时，`principal:dynamodb` 的流记录上会有一个属性。使用订阅到 DynamoDB Streams 的 Lambda 订阅用户，我们可以仅对 DynamoDB 主体属性应用事件筛选条件，并且可以确定，与该筛选条件匹配的所有记录都将推送到存档存储中，例如 Amazon Glacier。

**此构建基块的主要特点**
+  对于历史项目，当不再需要 DynamoDB 的低延迟读取时，将其迁移到 Amazon Glacier 等冷存储中可以显著降低存储成本，同时满足使用案例的数据合规性需求 
+ 如果将数据保存到 Amazon S3 中，则可以使用 Amazon Athena 或 Redshift Spectrum 等经济高效的分析工具，对数据执行历史分析

## 垂直分区构建基块
<a name="data-modeling-blocks-vertical-partitioning"></a>

用户如果熟悉文档模型数据库，那么也会熟悉将所有相关数据存储在单个 JSON 文档中的概念。尽管 DynamoDB 支持 JSON 数据类型，但不支持在嵌套 JSON 上执行 `KeyConditions`。由于 `KeyConditions` 决定从磁盘读取的数据量以及实际上查询使用的 RCU 数量，因此在大规模执行时，这可能会导致效率低下。为了更好地优化 DynamoDB 的写入和读取，我们建议将文档的单独实体拆分为单独的 DynamoDB 项目，这种方法也称为**垂直分区**。

![\[图中显示了格式化为嵌套的 JSON 对象的大型数据结构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DocumentBlob.png)


![\[图中显示了项目集合，其中项目的排序键有助于优化 DynamoDB 的使用。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SingleTableSchema.png)


如上所示，垂直分区是单表设计在实际使用中的一个重要例子，但在需要时，也可以在多个表中实施。由于 DynamoDB 对写入以 1KB 为单位进行计费，因此理想情况下，您在对文档分区时，应该使得生成的项目小于 1KB。

**此构建基块的主要特点**
+ 数据关系的层次结构通过排序键前缀维护，因此在需要时，可以在客户端重建单一文档结构 
+ 数据结构的单个组件可以独立更新，因此可以进行只需一个 WCU 的小项目更新 
+ 通过使用排序键 `BeginsWith`，应用程序可以在单个查询中检索相似的数据，聚合读取成本以降低总成本/延迟
+ 大型文档很容易超过 DynamoDB 中 400 KB 的单个项目大小限制，垂直分区有助于解决这个限制

## 写入分片构建基块
<a name="data-modeling-blocks-write-sharding"></a>

DynamoDB 有很少几个硬性限制，其中之一是限制单个物理分区可以保持的每秒吞吐量（不一定是单个分区键）。目前，这些限制是：
+ 1000 个 WCU（或每秒写入 1000 个 <=1KB 的项目）和 3000 个 RCU（或每秒 3000 个 <=4KB 的读取）（*强一致性*），或者 
+ 每秒 6000 个 <=4KB 的读取（*最终一致性*）

如果对表的请求数量超过上述任一限制，则会将错误 `ThroughputExceededException` 发送回客户端 SDK，这种情况通常称为节流。如果使用场景需要的读取操作数超出该限制，最适合的处理方法是在 DynamoDB 前面放置读取缓存，但写入操作需要采用称为**写入分片**的架构级别设计。

![\[图中显示 DynamoDB 如何跨多个分区，对分区键进行分片以防止流量高峰导致的节流。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/WriteShardingProblem.png)


![\[图中显示 DynamoDB 如何跨多个分区，对分区键进行分片以防止流量高峰导致的节流。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/WriteShardingSolution.png)


为了解决这个问题，我们将在应用程序的 `UpdateItem` 代码中，对每个参赛选手的分区键末尾附加一个随机整数。对于随机整数生成器，其范围上限必须等于或大于给定参赛选手预期的每秒写入数除以 1000。要支持每秒 2 万次投票，其上限应该类似于 rand(0,19)。现在，数据存储在单独的逻辑分区下，读取时必须将其重新合并在一起。由于投票总数不必是实时结果，因此 Lambda 函数计划每 X 分钟读取一次所有投票分区，这可以不定期地对每位参赛选手执行聚合操作，并将其写回单个投票总数记录以进行实时读取。

**此构建基块的主要特点**
+ 如果在使用场景中，对于给定分区键具有极高的写入吞吐量且无法避免，可以人为地将写入操作分散到多个 DynamoDB 分区上 
+ 具有低基数分区键的 GSI 也应使用这种模式，因为 GSI 上的节流会对基表的写入操作带来反向压力

# DynamoDB 中的数据建模架构设计包
<a name="data-modeling-schemas"></a>

了解 DynamoDB 的数据建模架构设计包，包括社交网络、游戏个人资料、投诉管理、定期付款、设备状态和在线商店的应用场景、访问模式和最终架构设计。

![\[图中显示了数据、位于数据下方的数据块以及位于数据块下方的基础之间的概念关系。重点强调基础。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SchemaDesignData.png)


## 先决条件
<a name="data-modeling-prereqs"></a>

在我们尝试为 DynamoDB 设计架构之前，我们首先必须针对该架构需要支持的使用场景，收集一些先决条件数据。与关系数据库不同，DynamoDB 在默认情况下采用分片模式，这意味着数据在后台位于多个服务器上，因此针对数据局部性进行设计非常重要。我们需要为每种架构设计整理以下列表：
+ 实体列表（ER 图）
+ 每个实体的估计数量和吞吐量
+ 需要支持的访问模式（查询和写入）
+ 数据留存要求

**Topics**
+ [先决条件](#data-modeling-prereqs)
+ [DynamoDB 中的社交网络架构设计](data-modeling-schema-social-network.md)
+ [DynamoDB 中的游戏个人资料架构设计](data-modeling-schema-gaming-profile.md)
+ [DynamoDB 中的投诉管理系统架构设计](data-modeling-complaint-management.md)
+ [DynamoDB 中的定期付款架构设计](data-modeling-schema-recurring-payments.md)
+ [在 DynamoDB 中监控设备状态更新](data-modeling-device-status.md)
+ [使用 DynamoDB 作为在线商店的数据存储](data-modeling-online-shop.md)

# DynamoDB 中的社交网络架构设计
<a name="data-modeling-schema-social-network"></a>

## 社交网络业务使用场景
<a name="data-modeling-schema-social-network-use-case"></a>

此使用场景探讨如何将 DynamoDB 用于社交网络。社交网络是一种在线服务，让不同的用户可以彼此互动。我们设计的社交网络将提供一个时间线供用户查看，其中包括他们的帖子、他们的粉丝、他们关注的人以及他们关注的人的发帖。此架构设计的访问模式为：
+ 获取给定 userID 的用户信息 
+ 获取给定 userID 的粉丝名单
+ 获取给定 userID 的关注名单
+ 获取给定 userID 的帖子列表
+ 获取给定 postID 中喜欢该帖子的用户名单
+ 获取给定 postID 的点赞次数
+ 获取给定 userID 的时间线

## 社交网络实体关系图
<a name="data-modeling-schema-social-network-erd"></a>

这是我们在社交网络架构设计中使用的实体关系图 (ERD, Entity Relationship Diagram)。

![\[ERD 表示一个显示实体（例如用户、帖子和粉丝）的社交网络应用程序。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetworkERD.png)


## 社交网络访问模式
<a name="data-modeling-schema-social-network-access-patterns"></a>

我们将为社交网络架构设计考虑这些访问模式。
+ `getUserInfoByUserID`
+ `getFollowerListByUserID`
+ `getFollowingListByUserID`
+ `getPostListByUserID`
+ `getUserLikesByPostID`
+ `getLikeCountByPostID`
+ `getTimelineByUserID`

## 社交网络架构设计演变
<a name="data-modeling-schema-social-network-design-evolution"></a>

DynamoDB 是一个 NoSQL 数据库，因此不允许您执行联接操作，也就是合并来自多个数据库的数据的操作。不熟悉 DynamoDB 的客户可能会不必要地将关系数据库管理系统 (RDBMS, Relational DataBase Management System) 的设计理念（例如为每个实体创建表）应用于 DynamoDB。DynamoDB 单表设计的目的是根据应用程序的访问模式，以预先联接的形式写入数据，然后无需额外计算即可立即使用数据。有关更多信息，请参阅 [DynamoDB 中的单表与多表设计。](https://aws.amazon.com/blogs/database/single-table-vs-multi-table-design-in-amazon-dynamodb/)

现在，我们来逐步了解一下如何改进架构设计以解决所有访问模式。

**步骤 1：解决访问模式 1 (`getUserInfoByUserID`)**

要获取给定用户的信息，我们需要使用键条件 `PK=<userID>` 对基表执行 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html) 操作。查询操作允许您对结果进行分页，这在用户有很多关注者时很有用。有关 Query 的更多信息，请参阅[在 DynamoDB 中查询表](Query.md)。

在示例中，我们跟踪用户的两种类型的数据：他们的“count”和“info”。用户的“count”反映了他们有多少粉丝，他们关注了多少用户，以及他们创建了多少帖子。用户的“info”反映了他们的个人信息，例如他们的姓名。

我们看到这两种数据类型由以下两项表示。排序键 (SK) 中带有“count”的项目比带有“info”的项目更有可能发生改变。DynamoDB 会考虑更新前后的项目大小，所消耗的预置吞吐量将反映这些项目大小中较大的一个。因此，即使您只更新项目属性的子集，[https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) 仍会消耗全部的预调配吞吐量（之前和之后项目大小中的较大者）。您可以通过单个 `Query` 操作获取项目，并使用 `UpdateItem` 对现有的数值属性进行加减。

![\[ID 为 u#12345 的用户的查询操作结果及其 count 和 info 数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork1.png)


**步骤 2：解决访问模式 2 (`getFollowerListByUserID`)**

要获取关注给定用户的用户名单，我们需要使用键条件 `PK=<userID>#follower` 对基表执行 `Query` 操作。

![\[查询操作的结果，对表执行此查询操作来列出 ID 为 u#12345 的用户的粉丝。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork2.png)


**步骤 3：解决访问模式 3 (`getFollowingListByUserID`)**

要获取给定用户所关注的用户名单，我们需要使用键条件 `PK=<userID>#following` 对基表执行 `Query` 操作。接下来，您可以使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html) 操作来将多个请求分组在一起，并执行以下操作：
+ 将用户 A 添加到用户 B 的粉丝名单，然后将用户 B 的粉丝人数增加 1。
+ 将用户 B 添加到用户 A 的粉丝名单，然后将用户 A 的粉丝人数增加 1。

![\[查询操作的结果，对表执行此查询操作来列出 ID 为 u#12345 的用户正在关注的所有用户。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork3.png)


**步骤 4：解决访问模式 4 (`getPostListByUserID`)**

要获取给定用户发布的帖子列表，我们需要使用键条件 `PK=<userID>#post` 对基表执行 `Query` 操作。这里有非常重要的一点需要注意，用户的 postID 必须是递增的：第二个 postID 值必须大于第一个 postID 值（因为用户希望以排序的方式查看他们的帖子）。为此，您可以根据时间值生成 postID 来实现此目的，例如通用唯一词典排序标识符 (ULID, Lexicographically Sortable Identifier)。

![\[查询操作的结果，此查询操作使用键条件来获取由特定用户创建的帖子列表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork4.png)


**步骤 5：解决访问模式 5 (`getUserLikesByPostID`)**

要获取对给定用户的帖子点赞的用户名单，我们需要使用键条件 `PK=<postID>#likelist` 对基表执行 `Query` 操作。这种方法与我们在访问模式 2 (`getFollowerListByUserID`) 和访问模式 3 (`getFollowingListByUserID`) 中用来检索粉丝和关注名单时使用的模式相同。

![\[查询操作的结果，此查询操作使用键条件来获取对特定用户的帖子点赞的用户列表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork5.png)


**步骤 6：解决访问模式 6 (`getLikeCountByPostID`)**

要获取给定帖子的点赞数，我们需要使用键条件 `PK=<postID>#likecount` 对基表执行 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) 操作。当拥有许多粉丝的某个用户（例如名人）创建帖子时，这种访问模式可能会导致节流问题，因为当单个分区的吞吐量超过每秒 1000 个 WCU 时，就会出现节流。这个问题不是 DynamoDB 造成的，只是正好出现在 DynamoDB 中，因为它位于软件堆栈的最后部分。

您应该评估一下是否有必要让所有用户同时查看点赞数，还是可以在一段时间内逐步显示。通常，帖子的点赞数不必立即获得 100% 准确的值。您可以通过在应用程序和 DynamoDB 之间放置队列，用于定期进行更新，以此来实施此策略。

![\[GetItem 操作的结果，此操作使用键条件来获取特定帖子的点赞数。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork6.png)


**步骤 7：解决访问模式 7 (`getTimelineByUserID`)**

要获取给定用户的时间线，我们需要使用键条件 `PK=<userID>#timeline` 对基表执行 `Query` 操作。我们来考虑一个场景，即用户的粉丝需要同步查看用户的帖子。每次用户发了一个帖子时，都会读取用户的粉丝名单，并将用户的 userID 和 postID 逐步输入其所有粉丝的时间线中。然后，当应用程序启动时，您可以通过 `Query` 操作读取时间线键，并对任何新项目使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html) 操作，将 userID 和 postID 的组合结果来填充时间线屏幕。您无法通过 API 调用来读取时间线，但是如果帖子经常会进行编辑，则这是一种更具成本效益的解决方案。

时间线是显示最近帖子的位置，所以我们需要一种方法来清理旧帖子。您可以使用 DynamoDB 的 [TTL](TTL.md) 功能来免费删除旧帖子，而无需使用 WCU。

![\[查询操作的结果，此查询操作使用键条件来获取给定用户显示其最近帖子的时间表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork7.png)


下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 其他条件/筛选条件 | 
| --- | --- | --- | --- | --- | --- | 
| getUserInfoByUserID | 基表 | Query | PK=<userID> |  |  | 
| getFollowerListByUserID | 基表 | Query | PK=<userID>\$1follower |  |  | 
| getFollowingListByUserID | 基表 | Query | PK=<userID>\$1following |  |  | 
| getPostListByUserID | 基表 | Query | PK=<userID>\$1post |  |  | 
| getUserLikesByPostID | 基表 | Query | PK=<postID>\$1likelist |  |  | 
| getLikeCountByPostID | 基表 | GetItem | PK=<postID>\$1likecount |  |  | 
| getTimelineByUserID | 基表 | Query | PK=<userID>\$1timeline |  |  | 

## 社交网络最终架构
<a name="data-modeling-schema-social-network-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 示例](https://github.com/aws-samples/aws-dynamodb-examples/blob/master/schema_design/SchemaExamples/SocialNetwork/SocialNetworkSchema.json)。

**基表：**

![\[包含先前查询和 GetItem 操作结果的表的最终架构设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/SocialNetwork8.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-social-network-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。

# DynamoDB 中的游戏个人资料架构设计
<a name="data-modeling-schema-gaming-profile"></a>

## 游戏个人资料业务使用场景
<a name="data-modeling-schema-gaming-profile-use-case"></a>

此使用场景探讨使用 DynamoDB 来存储游戏系统的玩家个人资料。许多现代游戏，尤其是在线游戏，需要用户（在本例中为玩家）先创建个人资料，然后才能玩游戏。游戏个人资料通常包括以下内容：
+ 基本信息，例如玩家的用户名
+ 游戏数据，例如物品和装备
+ 游戏记录，例如任务和活动
+ 社交信息，例如好友名单

为了满足此应用程序的细粒度数据查询访问要求，主键（分区键和排序键）将使用通用名称（PK 和 SK），因此它们可能会使用各种类型的值重载，如下所示。

此架构设计的访问模式为：
+ 获取用户的好友名单
+ 获取玩家的所有信息
+ 获取用户的物品清单
+ 从用户的物品清单中获取特定物品
+ 更新用户的角色
+ 更新用户的物品数量

在不同的游戏中，游戏个人资料的大小会有所不同。[压缩大型属性值](bp-use-s3-too.md)可以让属性值符合 DynamoDB 的项目限制，从而降低成本。吞吐量管理策略取决于多种因素，例如：玩家数量、每秒玩的游戏数量以及工作负载的季节性。通常，对于新推出的游戏，玩家数量和受欢迎程度是未知的，因此我们一开始使用[按需吞吐量模式](capacity-mode.md#capacity-mode-on-demand)。

## 游戏个人资料实体关系图
<a name="data-modeling-schema-gaming-profile-erd"></a>

这是我们在游戏个人资料架构设计中使用的实体关系图（ERD）。

![\[游戏个人资料的 ER 图，显示 User、Game 和 Score 等实体之间的关系。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/GamingProfileERD.png)


## 游戏个人资料访问模式
<a name="data-modeling-schema-gaming-profile-access-patterns"></a>

我们将为社交网络架构设计考虑这些访问模式。
+ `getPlayerFriends`
+ `getPlayerAllProfile`
+ `getPlayerAllItems`
+ `getPlayerSpecificItem`
+ `updateCharacterAttributes`
+ `updateItemCount`

## 游戏个人资料架构设计演变
<a name="data-modeling-schema-social-network-design-evolution"></a>

从上面的 ERD 可以看出，这是一对多关系类型的数据建模。在 DynamoDB 中，一对多数据模型可以整理为项目集合，这不同于通过创建多个表并使用外键来链接的传统关系数据库。[项目集合](WorkingWithItemCollections.md)是一组共享相同分区键值但具有不同排序键值的项目。在项目集合中，每个项目都有一个唯一的排序键值，该值将其与其他项目区分开来。考虑到这一点，我们可以为每种实体类型的 `HASH` 和 `RANGE` 值使用以下模式。

首先，我们使用 `PK` 和 `SK` 等通用名称，将不同类型的实体存储在同一个表中，以便模型可供未来使用。为了提高可读性，我们可以添加前缀来表示数据的类型，也可以提供名为 `Entity_type` 或 `Type` 的任意属性。在当前示例中，我们使用以 `player` 开头的字符串，将 `player_ID` 存储为 `PK`；将 `entity name#` 用作 `SK` 的前缀，然后添加一个 `Type` 属性来指示这段数据所属的实体类型。这样，我们以后便能够支持存储更多的实体类型，并使用 GSI 重载和稀疏 GSI 等高级技术来满足更多的访问模式。

让我们开始实施访问模式。添加玩家和添加装备等访问模式可以通过 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) 操作实现，因此我们可以忽略这些访问模式。在本文中，我们将重点介绍上面列出的典型访问模式。

**步骤 1：解决访问模式 1 (`getPlayerFriends`)**

我们通过此步骤解决访问模式 1 (`getPlayerFriends`)。在我们目前的设计中，好友关系很简单，游戏中的好友数量很少。为简单起见，我们使用列表数据类型来存储好友列表（1:1 建模）。在此设计中，我们使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) 来满足这种访问模式。在 `GetItem` 操作中，我们明确提供分区键和排序键值以获取特定项目。

但是，如果某个游戏中有大量好友，并且他们之间的关系很复杂（例如好友关系是双向的，包括邀请和接受组件），则必须使用多对多关系来单独存储每个好友，这样才能扩展到大小不受限的好友名单。而且，如果好友关系变更涉及同时对多个项目进行操作，则可以使用 DynamoDB 事务将多个操作分组在一起，并将它们作为“全有或全无”的单个 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html) 或 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html) 操作提交。

![\[游戏个人资料中 Friends 实体的复杂多对多关系图。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/GamingProfile1.png)


**步骤 2：解决访问模式 2 (`getPlayerAllProfile`)、3 (`getPlayerAllItems`) 和 4 (`getPlayerSpecificItem`)**

我们使用此步骤解决访问模式 2 (`getPlayerAllProfile`)、3 (`getPlayerAllItems`) 和 4 (`getPlayerSpecificItem`)。这三种访问模式的共同点是使用 [`Query`](Query.md) 操作进行范围查询。根据查询的范围，使用[键条件](Query.KeyConditionExpressions.md)和[筛选表达式](Query.FilterExpression.md)，这些方法在实际开发中经常使用。

在查询操作中，我们为分区键提供一个值，然后获取具有该分区键值的所有项目。访问模式 2 (`getPlayerAllProfile`) 以这种方式实施。或者，我们可以添加排序键条件表达式，这是一个字符串，用于确定要从表中读取的项目。访问模式 3 (`getPlayerAllItems`) 通过添加键条件排序键 begins\$1with `ITEMS#` 来实施。此外，为了简化应用程序端的开发，我们可以使用筛选条件表达式来实施访问模式 4 (`getPlayerSpecificItem`)。

以下是使用筛选条件表达式筛选 `Weapon` 类别项目的伪代码示例：

```
filterExpression: "ItemType = :itemType"
expressionAttributeValues: {":itemType": "Weapon"}
```

![\[使用带有分区键和排序键条件的查询操作来实现不同的访问模式。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/GamingProfile2.png)


**注意**  
筛选表达式在查询已完成但结果尚未返回到客户端时应用。因此，无论是否存在筛选表达式，查询都将占用同等数量的读取容量。

如果访问模式是查询大型数据集，需要筛选掉大量数据，仅保留一小部分数据，则合适的方法是设计更有效的 DynamoDB 分区键和排序键。例如，在上面的获取特定 `ItemType` 的示例中，如果每个玩家都有很多物品并且典型的访问模式是查询特定的 `ItemType`，那么将 `ItemType` 引入 `SK` 中作为复合键会更有效率。数据模型类似于以下所示：`ITEMS#ItemType#ItemId`。

**步骤 3：解决访问模式 5 (`updateCharacterAttributes`) 和 6 (`updateItemCount`)**

我们使用此步骤解决访问模式 5 (`updateCharacterAttributes`) 和 6 (`updateItemCount`)。当玩家需要修改角色时，例如减少货币或修改其物品中特定武器的数量，可以使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) 来实施这些访问模式。在更新玩家货币时，为了确保金额永远不会低于最低数量，我们可以添加一个 [DynamoDB 条件表达式 CLI 示例](Expressions.ConditionExpressions.md)，确保只有其大于或等于最低金额时才减少余额。以下是伪代码示例：

```
UpdateExpression: "SET currency = currency - :amount"
ConditionExpression: "currency >= :minAmount"
```

![\[使用带有条件表达式的 UpdateItem 来修改玩家的货币，确保其金额从不会低于设定的金额。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/GamingProfile4-Update-player-Currency.png)


在使用 DynamoDB 进行开发并使用[原子计数器](WorkingWithItems.md#WorkingWithItems.AtomicCounters)减少库存时，我们可以通过使用乐观锁来确保幂等性。以下是原子计数器的伪代码示例：

```
UpdateExpression: "SET ItemCount = ItemCount - :incr"
expression-attribute-values: '{":incr":{"N":"1"}}'
```

![\[使用原子计数器将 ItemCount 属性值从 5 减至 4。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/GamingProfile5-Update-Item-Count.png)


此外，在玩家用货币购买物品的情况下，整个流程中需要扣除货币并同时添加物品。我们可以使用 DynamoDB 事务将多个操作分组在一起，并将它们作为“全有或全无”的单个 `TransactWriteItems` 或 `TransactGetItems` 操作提交。`TransactWriteItems` 是同步的幂等性写入操作，可将最多 100 个写入操作分组到一个“全有或全无”操作中。这些操作以原子方式完成，以便所有操作都成功或都失败。事务有助于消除重复或货币消失的风险。有关事务的更多信息，请参阅[DynamoDB 事务示例](transaction-example.md)。

下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 其他条件/筛选条件 | 
| --- | --- | --- | --- | --- | --- | 
| getPlayerFriends | 基表 | GetItem | PK=PlayerID | SK=“FRIENDS\$1playerID” |  | 
| getPlayerAllProfile | 基表 | Query | PK=PlayerID |  |  | 
| getPlayerAllItems | 基表 | Query | PK=PlayerID | SK begins\$1with “ITEMS\$1” |  | 
| getPlayerSpecificItem | 基表 | Query | PK=PlayerID | SK begins\$1with “ITEMS\$1” | filterExpression: "ItemType = :itemType" expressionAttributeValues: \$1 ":itemType": "Weapon" \$1 | 
| updateCharacterAttributes | 基表 | UpdateItem | PK=PlayerID | SK=“\$1METADATA\$1playerID” | UpdateExpression: "SET currency = currency - :amount" ConditionExpression: "currency >= :minAmount" | 
| updateItemCount | 基表 | UpdateItem | PK=PlayerID | SK =“ITEMS\$1ItemID” | update-expression: "SET ItemCount = ItemCount - :incr" expression-attribute-values: '\$1":incr":\$1"N":"1"\$1\$1' | 

## 游戏个人资料最终架构
<a name="data-modeling-schema-gaming-profile-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 示例](https://github.com/aws-samples/aws-dynamodb-examples/blob/master/schema_design/SchemaExamples/GamingPlayerProfiles/GamePlayerProfilesSchema.json)。

**基表：**

![\[包含上述访问模式实现结果的表的最终架构设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/GamingProfile6-FinalSchema.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-gaming-profile-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。

# DynamoDB 中的投诉管理系统架构设计
<a name="data-modeling-complaint-management"></a>

## 投诉管理系统业务使用场景
<a name="data-modeling-schema-complaint-management-use-case"></a>

DynamoDB 是一个非常适合投诉管理系统（或联系中心）使用场景的数据库，因为与之关联的大多数访问模式都是基于键/值的事务性查找。在这种情况下，典型的访问模式将是：
+ 创建和更新投诉
+ 上报投诉
+ 创建和阅读对投诉的评论
+ 收到客户的所有投诉
+ 获取客服坐席的所有评论并获取所有上报 

有些评论可能有描述投诉或解决方案的附件。虽然这些都是键/值访问模式，但可能还有其他要求，例如在投诉中添加新评论时发送通知，或者运行分析查询以每周按严重程度（或客服坐席绩效）查找投诉分布情况。与生命周期管理或合规性相关的另一项要求是在记录投诉三年后归档投诉数据。

## 投诉管理系统架构图
<a name="data-modeling-schema-complaint-management-ad"></a>

下图显示投诉管理系统的架构图。此图显示了投诉管理系统使用的不同 AWS 服务集成。

![\[组合的工作流程，通过与多个 AWS 服务的集成来满足非事务性需求。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-1-AD.jpg)


除了我们稍后将在 DynamoDB 数据建模部分中处理的键/值事务性访问模式外，我们还有三项非事务性要求。上面的架构图可以分解为以下三个工作流程：

1. 在投诉中添加新评论时发送通知

1. 对每周数据运行分析查询

1. 归档超过三年的数据

让我们更深入地了解每个工作流程。

**在投诉中添加新评论时发送通知**

我们可以使用以下工作流程来满足这项要求：

![\[调用 Lambda 函数来根据 DynamoDB Streams 记录的更改发送通知的工作流程。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-2-Workflow1.jpg)


[DynamoDB Streams](Streams.md) 是一种更改数据捕获机制，用于记录 DynamoDB 表上的所有写入活动。您可以配置 Lambda 函数以触发部分或全部更改。可以在 Lambda 触发器上配置[事件筛选条件](https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html)，以筛选掉与应用场景无关的事件。在这种情况下，只有在添加新评论时，我们才能使用筛选条件来触发 Lambda，并将通知发送到相关电子邮件 ID（可以从 [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) 或任何其他凭证存储中获取此类 ID）。

**对每周数据运行分析查询**

DynamoDB 适用于主要侧重于在线事务处理（OLTP）的工作负载。对于其他 10-20% 具有分析需求的访问模式，可以使用托管式[导出到 Amazon S3](S3DataExport.HowItWorks.md) 功能将数据导出到 S3，而不会影响 DynamoDB 表上的实时流量。看看下面的这个工作流程：

![\[定期调用 Lambda 函数来将 DynamoDB 数据存储在 Amazon S3 存储桶中的工作流程。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-3-Workflow2.jpg)


[Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is) 可用来按计划触发 AWS Lambda - 它允许您配置 cron 表达式，以便定期进行 Lambda 调用。Lambda 可以调用 `ExportToS3` API 调用并在 S3 中存储 DynamoDB 数据。然后，可以通过 SQL 引擎（例如 [Amazon Athena](https://docs.aws.amazon.com/athena/latest/ug/what-is)）访问此 S3 数据，以便在不影响表上的实时事务性工作负载的情况下，对 DynamoDB 数据运行分析查询。用于查找每个严重性级别的投诉数量的 Athena 查询示例如下所示：

```
SELECT Item.severity.S as "Severity", COUNT(Item) as "Count"
FROM "complaint_management"."data"
WHERE NOT Item.severity.S = ''
GROUP BY Item.severity.S ;
```

这将导致以下 Athena 查询结果：

![\[Athena 查询结果显示了严重性级别为 P3、P2 和 P1 的投诉数量。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-4-Athena.png)


**归档超过三年的数据**

您可以利用 DynamoDB [生存时间（TTL）](TTL.md)功能从 DynamoDB 表中删除过时数据，而无需任何额外费用 [2019.11.21（当前）版本的全局表副本除外，其中，复制到其他区域的 TTL 删除操作会消耗写入容量]。这些数据会出现，并且可以从 DynamoDB Streams 中使用，以归档到 Amazon S3 中。此要求的工作流程如下：

![\[使用 TTL 功能和 DynamoDB Streams 将旧数据归档到 Amazon S3 存储桶中的工作流程。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-5-Workflow3.jpg)


## 投诉管理系统实体关系图
<a name="data-modeling-schema-complaint-management-erd"></a>

这是我们将在投诉管理系统架构设计中使用的实体关系图（ERD）。

![\[投诉管理系统 ERD，显示 Customer、Complaint、Comment 和 Agent 等实体。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-6-ERD.jpg)


## 投诉管理系统访问模式
<a name="data-modeling-schema-complaint-management-access-patterns"></a>

这些是我们将在投诉管理架构设计中考虑的访问模式。

1. createComplaint

1. updateComplaint

1. updateSeveritybyComplaintID

1. getComplaintByComplaintID

1. addCommentByComplaintID

1. getAllCommentsByComplaintID

1. getLatestCommentByComplaintID

1. getAComplaintbyCustomerIDAndComplaintID

1. getAllComplaintsByCustomerID

1. escalateComplaintByComplaintID

1. getAllEscalatedComplaints

1. getEscalatedComplaintsByAgentID（按最新到最旧排序）

1. getCommentsByAgentID（在两个日期之间）

## 投诉管理系统架构设计演变
<a name="data-modeling-schema-complaint-management-design-evolution"></a>

由于这是一个投诉管理系统，因此大多数访问模式都围绕作为主要实体的投诉展开。`ComplaintID` 是高度基数的，这将确保数据在底层分区中均匀分布，也是我们识别的访问模式的最常见搜索标准。因此，`ComplaintID` 是该数据集中的理想分区键候选对象。

**步骤 1：解决访问模式 1（`createComplaint`）、2（`updateComplaint`）、3（`updateSeveritybyComplaintID`）和 4（`getComplaintByComplaintID`）**

我们可以使用名为“metadata”（或“AA”）的通用排序键值来存储特定于投诉的信息，例如 `CustomerID`、`State`、`Severity` 以及 `CreationDate`。我们对 `PK=ComplaintID` 和 `SK=“metadata”` 使用单例操作来执行以下操作：

1. 使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) 以创建新的投诉

1. 使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) 以更新投诉元数据中的严重性或其他字段

1. 使用 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) 以便为投诉获取元数据

![\[投诉项目的主键、排序键和属性值，例如 customer_id 和严重性。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-7-Step1.png)


**步骤 2：解决访问模式 5（`addCommentByComplaintID`）**

这种访问模式要求在投诉和投诉评论之间建立一对多关系模型。我们将在这里使用[垂直分区](data-modeling-blocks.md#data-modeling-blocks-vertical-partitioning)技术来使用排序键，并创建具有不同类型数据的项目集合。如果我们看一下访问模式 6（`getAllCommentsByComplaintID`）和 7（`getLatestCommentByComplaintID`），我们就知道评论需要按时间排序。我们也可以同时发表多条评论，这样我们就可以使用[复合排序键](data-modeling-blocks.md#data-modeling-blocks-composite)技术，以便在排序键属性中追加时间和 `CommentID`。

处理此类可能的评论冲突的其他选择是增加时间戳的粒度，或添加一个增量数字作为后缀，而不是使用 `Comment_ID`。在这种情况下，我们将为与评论对应的项目的排序键值加上“comm\$1”前缀，以启用基于范围的操作。

我们还需要确保投诉元数据中的 `currentState` 反映添加新评论时的状态。添加评论可能表明投诉已分配给客服坐席或已得到解决，诸如此类。为了以要么全有、要么全无的方式在投诉元数据中捆绑注释的添加和当前状态的更新，我们将使用 [TransactWriteItems](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html) API。生成的表状态现在如下所示：

![\[使用复合排序键将投诉及其评论作为一对多关系进行存储的表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-8-Step2.png)


让我们在表中添加一些更多数据，并将 `ComplaintID` 作为 `PK` 中的一个单独字段添加，以便在 `ComplaintID` 上需要额外索引的情况下对模型进行未来验证。另请注意，一些评论可能有附件，我们会将其存储在 Amazon Simple Storage Service 中，仅在 DynamoDB 中保留其引用或 URL。保持事务数据库尽可能精简以优化成本和性能是一种最佳实践。现在的数据如下所示：

![\[包含投诉元数据以及与每个投诉关联的所有评论数据的表。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-9-Step3.png)


**步骤 3：解决访问模式 6（`getAllCommentsByComplaintID`）和 7（`getLatestCommentByComplaintID`)**

为了获得某个投诉的所有评论，我们可以对排序键使用具有 `begins_with` 条件的 [`query`](Query.md) 操作。使用这样的排序键条件可以帮助我们只读取所需的内容，而不是消耗额外的读取容量来读取元数据条目，然后承担筛选相关结果的开销。例如，具有 `PK=Complaint123` 和 `SK` begins\$1with `comm#` 的查询操作将返回以下内容，同时跳过元数据条目：

![\[使用仅显示投诉评论的排序键条件的查询操作结果。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-10-Step4.png)


由于我们在模式 7（`getLatestCommentByComplaintID`）中需要投诉的最新评论，让我们使用另外两个查询参数：

1. `ScanIndexForward` 应设置为 False 以获得按降序排序的结果

1. `Limit` 应设置为 1 以获得最新的（只有一个）评论

类似于访问模式 6（`getAllCommentsByComplaintID`），我们使用 `begins_with` `comm#` 作为排序键条件来跳过元数据条目。现在，您可以将查询操作与 `PK=Complaint123` 和 `SK=begins_with comm#`、`ScanIndexForward=False` 和 `Limit` 1 结合使用，在此设计上执行访问模式 7。结果将返回以下目标项目：

![\[使用排序键条件获取投诉的最新评论的查询操作结果。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-11-Step5.png)


让我们向表中添加更多虚拟数据。

![\[表中包含用于获取所收到投诉的最新评论的虚拟数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-12-Step6.png)


**步骤 4：解决访问模式 8（`getAComplaintbyCustomerIDAndComplaintID`）和 9（`getAllComplaintsByCustomerID`）**

访问模式 8 (`getAComplaintbyCustomerIDAndComplaintID`) 和 9 (`getAllComplaintsByCustomerID`) 引入了新的搜索条件：`CustomerID`。从现有表中提取它需要执行昂贵的 [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html) 来读取所有数据，然后针对相关的 `CustomerID` 筛选相关项目。我们可以通过创建一个以 `CustomerID` 为分区键的[全局二级索引（GSI）](GSI.md)来提高搜索效率。记住客户和投诉之间的一对多关系以及访问模式 9（`getAllComplaintsByCustomerID`），`ComplaintID` 将是排序键的正确候选对象。

GSI 中的数据将如下所示：

![\[此 GSI 采用一对多关系模型，来获取特定 CustomerID 的所有投诉。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-13-Step4-GSI.png)


 此 GSI 上用于访问模式 8（`getAComplaintbyCustomerIDAndComplaintID`）的查询示例将是：`customer_id=custXYZ`、`sort key=Complaint1321`。结果将是：

![\[对 GSI 执行的查询操作结果，该查询操作旨在获取给定客户的特定投诉数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-14-Step4-8.png)


获取客户对访问模式 9（`getAllComplaintsByCustomerID`）的所有投诉，GSI 上的查询将是：`customer_id=custXYZ` 作为分区键条件。结果将是：

![\[使用分区键条件来获取给定客户的所有投诉的查询操作结果。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-15-Step4-9.png)


**步骤 5：解决访问模式 10（`escalateComplaintByComplaintID`）**

这种访问引入了上报环节。要上报投诉，我们可以使用 `UpdateItem` 来将属性（例如 `escalated_to` 和 `escalation_time`）添加到现有的投诉元数据项目。DynamoDB 提供灵活的架构设计，这意味着一组非关键属性在不同的项目之间可以是统一的或离散的。有关示例，请参阅以下内容：

`UpdateItem with PK=Complaint1444, SK=metadata`

![\[使用 UpdateItem API 操作更新投诉元数据的结果。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-16-Step5.png)


**步骤 6：解决访问模式 11（`getAllEscalatedComplaints`）和 12（`getEscalatedComplaintsByAgentID`）**

预计整个数据集中只有少数投诉会上报。因此，对上报相关属性创建索引将带来高效的查找以及经济高效的 GSI 存储。我们可以利用[稀疏索引](data-modeling-blocks.md#data-modeling-blocks-sparse-index)技术来实现这一目标。分区键为 `escalated_to` 且排序键为 `escalation_time` 的 GSI 看起来像这样：

![\[使用与上报相关的属性（escalated_to 和 escalation_time）的 GSI 设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-17-Step6.png)


要获取所有针对访问模式 11（`getAllEscalatedComplaints`）的上报投诉，我们只需扫描这个 GSI 即可。请注意，由于 GSI 的大小，此扫描将具有高性能和成本效益。为了获得针对特定客服坐席的上报投诉 [访问模式 12（`getEscalatedComplaintsByAgentID`）]，分区键将为 `escalated_to=agentID`，并且我们将 `ScanIndexForward` 设置为 `False`，以便按最新到最旧排序。

**步骤 7：解决访问模式 13（`getCommentsByAgentID`)**

对于最后一个访问模式，我们需要按新维度进行查找：`AgentID`。我们还需要基于时间的排序来读取两个日期之间的评论，所以我们以 `agent_id` 作为分区键并以 `comm_date` 作为排序键创建一个 GSI。此 GSI 中的数据将如下所示：

![\[此 GSI 设计用于查找给定客服坐席的评论（使用评论日期进行排序）。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-18.png)


此 GSI 上的查询示例是 `partition key agentID=AgentA` 和 `sort key=comm_date between (2023-04-30T12:30:00, 2023-05-01T09:00:00)`，其结果是：

![\[使用分区键和排序键对 GSI 进行查询的结果。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-19.png)


下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 其他条件/筛选条件 | 
| --- | --- | --- | --- | --- | --- | 
| createComplaint | 基表 | PutItem | PK=complaint\$1id | SK=metadata |  | 
| updateComplaint | 基表 | UpdateItem | PK=complaint\$1id | SK=metadata |  | 
| updateSeveritybyComplaintID | 基表 | UpdateItem | PK=complaint\$1id | SK=metadata |  | 
| getComplaintByComplaintID | 基表 | GetItem | PK=complaint\$1id | SK=metadata |  | 
| addCommentByComplaintID | 基表 | TransactWriteItems | PK=complaint\$1id | SK=metadata，SK=comm\$1comm\$1date\$1comm\$1id |  | 
| getAllCommentsByComplaintID | 基表 | Query | PK=complaint\$1id | SK begins\$1with "comm\$1" |  | 
| getLatestCommentByComplaintID | 基表 | Query | PK=complaint\$1id | SK begins\$1with "comm\$1" | scan\$1index\$1forward=False，Limit 1 | 
| getAComplaintbyCustomerIDAndComplaintID | Customer\$1complaint\$1GSI | 查询 | customer\$1id=customer\$1id | complaint\$1id = complaint\$1id |  | 
| getAllComplaintsByCustomerID | Customer\$1complaint\$1GSI | 查询 | customer\$1id=customer\$1id | 不适用 |  | 
| escalateComplaintByComplaintID | 基表 | UpdateItem | PK=complaint\$1id | SK=metadata |  | 
| getAllEscalatedComplaints | Escalations\$1GSI | Scan | 不适用 | 不适用 |  | 
| getEscalatedComplaintsByAgentID（按最新到最旧排序） | Escalations\$1GSI | 查询 | escalated\$1to=agent\$1id | 不适用 | scan\$1index\$1forward=False | 
| getCommentsByAgentID（在两个日期之间） | Agents\$1Comments\$1GSI | 查询 | agent\$1id=agent\$1id | SK between (date1, date2) |  | 

## 投诉管理系统最终架构
<a name="data-modeling-schema-complaint-management-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 示例](https://github.com/aws-samples/aws-dynamodb-examples/blob/master/schema_design/SchemaExamples/ComplainManagement/ComplaintManagementSchema.json)。

**基表**

![\[带有投诉元数据的基表设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-20-Complaint_management_system.png)


**Customer\$1Complaint\$1GSI**

![\[此 GSI 设计显示特定客户的投诉。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-21-Customer_Complaint_GSI.png)


**Escalations\$1GSI**

![\[此 GSI 设计显示与上报相关的属性。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-22-Escalations_GSI.png)


**Agents\$1Comments\$1GSI**

![\[此 GSI 设计显示特定客服坐席发表的评论。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ComplaintManagement-23-Comments_GSI.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-complaint-management-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。

# DynamoDB 中的定期付款架构设计
<a name="data-modeling-schema-recurring-payments"></a>

## 定期付款业务使用场景
<a name="data-modeling-schema-recurring-payments-use-case"></a>

这个使用场景讨论了如何使用 DynamoDB 来实现定期付款系统。数据模型具有以下实体：*账户*、*订阅*以及*收据*。我们的使用场景的具体内容包括以下各项：
+ 每个*账户*可以有多个*订阅*
+ 当需要处理下一笔付款时，*订阅*具有 `NextPaymentDate`；当向客户发送电子邮件提醒时，则具有 `NextReminderDate`
+ *订阅*有一个项目，此项目在处理付款时存储并更新（平均项目大小约为 1KB，吞吐量取决于*账户*和*订阅*的数量）
+ *付款*处理器还将创建*收据*作为此过程的一部分，该收据存储在表中，并通过使用 [TTL](TTL.md) 属性设置为在一段时间后过期

## 定期付款实体关系图
<a name="data-modeling-schema-recurring-payments-erd"></a>

这是我们将为定期付款系统架构设计使用的实体关系图（ERD）。

![\[定期付款系统 ERD 显示实体：Account、Subscription 和 Receipt。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-1-ERD.png)


## 定期付款系统访问模式
<a name="data-modeling-schema-recurring-payments-access-patterns"></a>

这些是我们将为定期付款系统架构设计考虑的访问模式。

1. `createSubscription`

1. `createReceipt`

1. `updateSubscription`

1. `getDueRemindersByDate`

1. `getDuePaymentsByDate`

1. `getSubscriptionsByAccount`

1. `getReceiptsByAccount`

## 定期付款架构设计
<a name="data-modeling-schema-recurring-payments-design-evolution"></a>

通用名称 `PK` 和 `SK` 用于键属性，以允许在同一个表中存储不同类型的实体，例如账户、订阅和收款实体。用户首先创建订阅，即用户同意在每个月的同一天支付一定金额以换取产品。他们可以选择在一个月的哪一天处理付款。在处理付款之前，还会发送提醒。该应用程序的工作原理是每天运行两个批处理任务：一个批处理任务发送当天到期的提醒，另一个批处理任务处理当天到期的任何付款。

**步骤 1：解决访问模式 1 (`createSubscription`)**

访问模式 1（`createSubscription`）用于最初创建订阅，并设置详细信息，包括 `SKU`、`NextPaymentDate`、`NextReminderDate` 和 `PaymentDetails`。此步骤仅显示一个账户（具有一个订阅）的表状态。项目集合中可能有多个订阅，因此这是一种一对多关系。

![\[这一表设计显示账户的订阅详细信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-2-Step1.png)


**步骤 2：解决访问模式 2（`createReceipt`）和 3（`updateSubscription`)**

访问模式 2（`createReceipt`）用于创建收据项目。每月处理付款后，付款处理器会将收据写回基表。项目集合中可能有多张收据，因此这是一对多关系。付款处理器还将更新订阅项目 [访问模式 3（`updateSubscription`)]，以针对下个月的 `NextReminderDate` 或 `NextPaymentDate` 进行更新。

![\[收据详细信息和订阅项目更新，来显示下一个订阅提醒日期。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-3-Step2.png)


**步骤 3：解决访问模式 4（`getDueRemindersByDate`）**

该应用程序会分批处理当天的付款提醒。因此，应用程序需要在不同的维度上访问订阅：日期而不是账户。这是[全局二级索引（GSI）](GSI.md)的一个很好的使用场景。在此步骤中，我们添加索引 `GSI-1`，它使用 `NextReminderDate` 作为 GSI 分区键。我们不需要复制所有项目。此 GSI 是一个[稀疏索引](data-modeling-blocks.md#data-modeling-blocks-sparse-index)，并且未复制收据项目。我们也不需要投影所有属性 — 我们只需要包含属性的子集。下图显示了 `GSI-1` 的架构，它提供了应用程序发送提醒电子邮件所需的信息。

![\[此 GSI-1 架构包含电子邮件地址等详细信息，应用程序需要这些信息来发送提醒电子邮件。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-4-Step3.png)


**步骤 4：解决访问模式 5（`getDuePaymentsByDate`）**

该应用程序以与对待提醒相同的方式批处理当天的付款。我们在此步骤中添加 `GSI-2`，它使用 `NextPaymentDate` 作为 GSI 分区键。我们不需要复制所有项目。此 GSI 是一个稀疏索引，因为未复制收据项目。下图显示了 `GSI-2` 的架构。

![\[此 GSI-2 架构包含处理付款的详细信息。NextPaymentDate 是 GSI-2 的分区键。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-5-Step4.png)


**步骤 5：解决访问模式 6（`getSubscriptionsByAccount`）和 7（`getReceiptsByAccount`）**

应用程序可以通过对以账户标识符（`PK`）为目标的基表使用[查询](Query.md)来检索账户的所有订阅，并使用范围运算符来获取 `SK` 以“SUB\$1”开头的所有项目。应用程序还可以使用相同的查询结构来检索所有收据，方法是使用范围运算符来获取其中 `SK` 以“REC\$1”开头的所有收据。这使我们能够满足访问模式 6（`getSubscriptionsByAccount`）和 7（`getReceiptsByAccount`）的要求。应用程序使用这些访问模式，因此，用户可以查看他们当前的订阅和过去六个月的收据。在此步骤中，表架构没有发生任何变化，我们可以在下面看到我们如何仅将访问模式 6（`getSubscriptionsByAccount`）中的订阅项目作为目标。

![\[对基表执行查询操作的结果。它显示特定账户的订阅。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-6-Step5.png)


下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 
| --- | --- | --- | --- | --- | 
| createSubscription | 基表 | PutItem | ACC\$1account\$1id | SUB\$1<SUBID>\$1SKU<SKUID> | 
| createReceipt | 基表 | PutItem | ACC\$1account\$1id | REC\$1<RecieptDate>\$1SKU<SKUID> | 
| updateSubscription | 基表 | UpdateItem | ACC\$1account\$1id | SUB\$1<SUBID>\$1SKU<SKUID> | 
| getDueRemindersByDate | GSI-1 | 查询 | <NextReminderDate> |  | 
| getDuePaymentsByDate | GSI-2 | 查询 | <NextPaymentDate> |  | 
| getSubscriptionsByAccount | 基表 | Query | ACC\$1account\$1id | SK begins\$1with “SUB\$1” | 
| getReceiptsByAccount | 基表 | Query | ACC\$1account\$1id | SK begins\$1with “REC\$1” | 

## 定期付款最终架构
<a name="data-modeling-schema-recurring-payments-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 示例](https://github.com/aws-samples/aws-dynamodb-examples/blob/master/schema_design/SchemaExamples/ReocurringPayments/ReocurringPaymentsSchema.json)。

**基表**

![\[此基表设计显示账户信息及其订阅和收据详细信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-7-Base.png)


**GSI-1**

![\[此 GSI-1 架构包含订阅详细信息，例如电子邮件地址和 NextPaymentDate。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-8-GSI1.png)


**GSI-2**

![\[此 GSI-2 架构包含付款详细信息，例如 PaymentAmount 和 PaymentDay。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/ReoccurringPayments-9-GSI2.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-recurring-payments-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。

# 在 DynamoDB 中监控设备状态更新
<a name="data-modeling-device-status"></a>

此使用案例讨论使用 DynamoDB 来监控 DynamoDB 中的设备状态更新（或设备状态更改）。

## 使用案例
<a name="data-modeling-schema-device-status-use-case"></a>

在 IoT 使用案例（例如智能工厂）中，许多设备需要由操作员监控，它们会定期将其状态或日志发送到监控系统。当设备出现问题时，该设备的状态会从*正常*更改为*警告*。根据设备中异常行为的严重程度和类型，日志级别或状态会有所不同。然后，系统会指派一名操作员检查设备，如果需要，操作员可以将问题上报给主管。

此系统的一些典型访问模式包括：
+ 为设备创建日志条目
+ 获取特定设备状态的所有日志，首先显示最新的日志
+ 获取给定操作员在两个日期之间的所有日志
+ 获取向给定主管上报的所有日志
+ 获取向给定主管上报的带有特定设备状态的所有日志
+ 获取特定日期向给定主管上报的带有特定设备状态的所有日志

## 实体关系图
<a name="data-modeling-schema-device-status-erd"></a>

这是我们将用于监控设备状态更新的实体关系图（ERD）。

![\[设备状态更新的 ERD。它显示了实体：Device 和 Operator。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-1-ERD.jpg)


## 访问模式
<a name="data-modeling-schema-device-status-access-patterns"></a>

这些是我们将考虑用于监控设备状态更新的访问模式。

1. `createLogEntryForSpecificDevice`

1. `getLogsForSpecificDevice`

1. `getWarningLogsForSpecificDevice`

1. `getLogsForOperatorBetweenTwoDates`

1. `getEscalatedLogsForSupervisor`

1. `getEscalatedLogsWithSpecificStatusForSupervisor`

1. `getEscalatedLogsWithSpecificStatusForSupervisorForDate`

## 架构设计的演变
<a name="data-modeling-schema-device-status-design-evolution"></a>

**步骤 1：解决访问模式 1（`createLogEntryForSpecificDevice`）和 2（`getLogsForSpecificDevice`）**

设备跟踪系统的扩展单位将是各个设备。在该系统中，`deviceID` 唯一地标识设备。这使得 `deviceID` 成为分区键的出色候选者。每个设备都会定期（比如每五分钟左右）向跟踪系统发送信息。这种排序使日期成为逻辑排序标准，因此成为排序键。本案例中的示例数据如下所示：

![\[存储多台设备的状态的表。DeviceID 是主键，状态更新日期是排序键。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-2-Step1.png)


要获取特定设备的日志条目，我们可以使用分区键 `DeviceID="d#12345"` 执行[查询](Query.md)操作。

**步骤 2：解决访问模式 3（`getWarningLogsForSpecificDevice`）**

由于 `State` 是一个非键属性，因此，使用当前架构解决访问模式 3 将需要一个[筛选表达式](Query.FilterExpression.md)。在 DynamoDB 中，筛选表达式是在使用键条件表达式读取数据之后应用的。例如，如果我们要获取 `d#12345` 的警告日志，那么分区键为 `DeviceID="d#12345"` 的查询操作将从上表中读取四个项目，然后筛选出一个没有*警告*状态的项目。这种方法在规模较大时效率不高。如果所排除的项目的比率较低或查询不频繁执行，则筛选表达式可能是一种排除所查询的项目的好方法。但是，对于从表中检索到许多项目并且大多数项目被筛选掉的情况，我们可以继续改进我们的表设计，使其运行效率更高。

让我们通过使用[复合排序键](data-modeling-blocks.md#data-modeling-blocks-composite)来更改处理这种访问模式的方式。您可以从 [DeviceStateLog\$13.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/device-state-log/json/DeviceStateLog_3.json) 导入示例数据，其中排序键更改为 `State#Date`。此排序键是属性 `State`、`#` 和 `Date` 的组合。在本例中，`#` 用作分隔符。数据现在如下所示：

![\[使用复合排序键 State#Date 获取的设备 d#12345 的状态更新数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-3-Step2.png)


要仅获取设备的警告日志，则使用此架构可以使查询更具针对性。查询的键条件使用分区键 `DeviceID="d#12345"` 和排序键 `State#Date begins_with “WARNING”`。此查询将只读取具有*警告*状态的相关三个项目。

**步骤 3：解决访问模式 4（`getLogsForOperatorBetweenTwoDates`）**

您可以导入 [DeviceStateLog\$14.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/device-state-log/json/DeviceStateLog_4.json)，其中 `Operator` 属性已添加到带有示例数据的 `DeviceStateLog` 表中。

![\[DeviceStateLog 表设计，带有 Operator 属性，用于获取操作员在特定日期之间的日志。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-4-Step3.png)


由于 `Operator` 当前不是分区键，因此无法基于 `OperatorID` 对该表执行直接键/值查找。我们需要在 `OperatorID` 上创建一个具有全局二级索引的新[项目集合](WorkingWithItemCollections.md)。访问模式需要基于日期的查找，因此 Date 是[全局二级索引（GSI）](GSI.md)的排序键属性。这就是 GSI 现在的样子：

![\[此 GSI 设计以 OperatorID 和 Date 作为分区键和排序键，来获取特定操作员的日志。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-5-Step3.png)


对于访问模式 4（`getLogsForOperatorBetweenTwoDates`），您可以在 `2020-04-11T05:58:00` 和 `2020-04-24T14:50:00` 之间使用分区键 `OperatorID=Liz` 和排序键 `Date` 来查询此 GSI。

![\[使用 OperatorID 和 Date 对 GSI 进行查询，来获取操作员在两个日期之间的日志。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-6-GSI1_1.png)


**步骤 4：解决访问模式 5（`getEscalatedLogsForSupervisor`）、6（`getEscalatedLogsWithSpecificStatusForSupervisor`）和 7（`getEscalatedLogsWithSpecificStatusForSupervisorForDate`）**

我们将使用[稀疏索引](data-modeling-blocks.md#data-modeling-blocks-sparse-index)来解决这些访问模式。

原定设置情况下，全局二级索引是稀疏索引，因此，只有基表中包含索引的主键属性的项目才会实际出现在索引中。这是排除与正在建模的访问模式无关的项目的另一种方法。

您可以导入 [DeviceStateLog\$16.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/device-state-log/json/DeviceStateLog_6.json)，其中 `EscalatedTo` 属性已添加到带有示例数据的 `DeviceStateLog` 表中。如前所述，并非所有日志都会上报给主管。

![\[此 GSI 设计使用 EscalatedTo 属性来获取主管的所有上报的日志。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-7-Step4.png)


现在可以创建一个新的 GSI，其中 `EscalatedTo` 是分区键，`State#Date` 是排序键。请注意，只有同时具有 `EscalatedTo` 和 `State#Date` 属性的项目才会出现在索引中。

![\[此 GSI 设计旨在获取所有具有 EscalatedTo 和 State#Date 属性的项目。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-8-Step4.png)


其余的访问模式汇总如下：

    对于访问模式 5（getEscalatedLogsForSupervisor），您可以使用分区键 EscalatedTo="Sara" 对上报 GSI 执行查询   对于访问模式 6（getEscalatedLogsWithSpecificStatusForSupervisor），您可以使用分区键 EscalatedTo="Sara" 和排序键 State\$1Date begins\$1with “WARNING” 对上报 GSI 执行查询    对于访问模式 7（getEscalatedLogsWithSpecificStatusForSupervisorForDate），您可以使用分区键 EscalatedTo="Sara" 和排序键 State\$1Date begins\$1with “WARNING4\$12020-04-27” 对上报 GSI 执行查询    

下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 其他条件/筛选条件 | 
| --- | --- | --- | --- | --- | --- | 
| createLogEntryForSpecificDevice | 基表 | PutItem | DeviceID=deviceId | State\$1Date=state\$1date |  | 
| getLogsForSpecificDevice | 基表 | Query | DeviceID=deviceId | State\$1Date begins\$1with "state1\$1" | ScanIndexForward = False | 
| getWarningLogsForSpecificDevice | 基表 | Query | DeviceID=deviceId | State\$1Date begins\$1with "WARNING" |  | 
| getLogsForOperatorBetweenTwoDates | GSI-1 | 查询 | Operator=operatorName | date1 和 date2 之间的日期 |  | 
| getEscalatedLogsForSupervisor | GSI-2 | 查询 | EscalatedTo=supervisorName |  |  | 
| getEscalatedLogsWithSpecificStatusForSupervisor | GSI-2 | 查询 | EscalatedTo=supervisorName | State\$1Date begins\$1with "state1\$1" |  | 
| getEscalatedLogsWithSpecificStatusForSupervisorForDate | GSI-2 | 查询 | EscalatedTo=supervisorName | State\$1Date begins\$1with "state1\$1date1" |  | 

## 最终架构
<a name="data-modeling-schema-device-status-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 示例](https://github.com/aws-samples/aws-dynamodb-examples/tree/master/schema_design/SchemaExamples)。

**基表**

![\[包含设备状态元数据（例如设备 ID、状态和日期）的基表设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-9-Table.png)


**GSI-1**

![\[GSI-1 设计。它显示了主键和属性：DeviceID、State#Date 和 State。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-10-GSI1.png)


**GSI-2**

![\[GSI-2 设计。它显示了主键和属性：DeviceID、Operator、Date 和 State。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/DeviceStatus-11-GSI2.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-device-status-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。

# 使用 DynamoDB 作为在线商店的数据存储
<a name="data-modeling-online-shop"></a>

此使用案例讨论了使用 DynamoDB 作为在线商店（或电子商店）的数据存储。

## 使用案例
<a name="data-modeling-schema-online-shop"></a>

在线商店可让用户浏览不同的商品并最终购买它们。根据生成的发票，客户可以使用折扣码或礼品卡付款，然后使用信用卡支付剩余金额。采购的商品将从几个仓库中的一个仓库中挑选，并发运到提供的地址。在线商店的典型访问模式包括：
+ 获取给定 customerId 的客户
+ 获取给定 productId 的商品
+ 获取给定 warehouseId 的仓库
+ 通过 productId 获取所有仓库的商品库存
+ 获取给定 orderId 的订单
+ 获取给定 orderId 的所有商品
+ 获取给定 orderId 的发票
+ 获取给定 orderId 的所有货件
+ 获取给定日期范围内给定 productId 的所有订单
+ 获取给定 invoiceId 的发票
+ 获取给定 invoiceId 的所有付款
+ 获取给定 shipmentId 的货件详细信息
+ 获取给定 warehouseId 的货件
+ 获取给定 warehouseId 的所有商品的库存
+ 获取给定日期范围内给定 customerId 的所有发票
+ 获取给定日期范围内给定 customerId 订购的所有商品

## 实体关系图
<a name="data-modeling-schema-online-shop-erd"></a>

这是实体关系图（ERD），我们将使用它来将 DynamoDB 建模为在线商店的数据存储。

![\[此 ERD 表示包含 Product、Order、Payment 和 Customer 等实体的在线商店数据模型。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-1-ERD.png)


## 访问模式
<a name="data-modeling-schema-online-shop-access-patterns"></a>

当使用 DynamoDB 作为在线商店的数据存储时，我们将考虑这些访问模式。

1. `getCustomerByCustomerId`

1. `getProductByProductId`

1. `getWarehouseByWarehouseId`

1. `getProductInventoryByProductId`

1. `getOrderDetailsByOrderId`

1. `getProductByOrderId`

1. `getInvoiceByOrderId`

1. `getShipmentByOrderId`

1. `getOrderByProductIdForDateRange`

1. `getInvoiceByInvoiceId`

1. `getPaymentByInvoiceId`

1. `getShipmentDetailsByShipmentId`

1. `getShipmentByWarehouseId`

1. `getProductInventoryByWarehouseId`

1. `getInvoiceByCustomerIdForDateRange`

1. `getProductsByCustomerIdForDateRange`

## 架构设计的演变
<a name="data-modeling-schema-online-shop-design-evolution"></a>

使用[NoSQL Workbench for DynamoDB](workbench.md)，导入 [AnOnlineShop\$11.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_1.json)，以创建名为 `AnOnlineShop` 的新数据模型和名为 `OnlineShop` 的新表。请注意，我们使用通用名称 `PK` 和 `SK` 作为分区键和排序键。这是一种用于将不同类型的实体存储在同一个表中的做法。

**步骤 1：解决访问模式 1 (`getCustomerByCustomerId`)**

导入 [AnOnlineShop\$12.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_2.json) 以处理访问模式 1（`getCustomerByCustomerId`）。有些实体与其他实体没有关系，因此我们将对它们使用相同的 `PK` 和 `SK` 值。在示例数据中，请注意，键使用前缀 `c#`，以便将 `customerId` 与稍后添加的其他实体区分开来。对于其他实体也重复这种做法。

为了解决这种访问模式，[https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) 操作可以与 `PK=customerId` 和 `SK=customerId` 结合使用。

**步骤 2：解决访问模式 2 (`getProductByProductId`)**

导入 [AnOnlineShop\$13.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_3.json)，以解决 `product` 实体的访问模式 2（`getProductByProductId`）。产品实体的前缀为 `p#`，并且使用了相同的排序键属性来存储 `customerID` 以及 `productID`。通用命名和[垂直分区](data-modeling-blocks.md#data-modeling-blocks-vertical-partitioning)允许我们创建这样的项目集合，以实现有效的单表设计。

为了解决这种访问模式，`GetItem` 操作可以与 `PK=productId` 和 `SK=productId` 结合使用。

**步骤 3：解决访问模式 3 (`getWarehouseByWarehouseId`)**

导入 [AnOnlineShop\$14.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_4.json)，以解决 `warehouse` 实体的访问模式 3（`getWarehouseByWarehouseId`）。我们目前已将 `customer`、`product` 和 `warehouse` 实体添加到同一个表中。它们使用前缀和 `EntityType` 属性进行区分。类型属性（或前缀命名）可提高模型的可读性。如果我们只是将不同实体的字母数字 ID 存储在同一属性中，可读性就会受到影响。在没有这些标识符的情况下，很难将一个实体与另一个实体区分开来。

为了解决这种访问模式，`GetItem` 操作可以与 `PK=warehouseId` 和 `SK=warehouseId` 结合使用。

**基表：**

![\[使用前缀和 EntityType 按仓库 ID 获取仓库数据的 DynamoDB 表设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-2-Step3.png)


**步骤 4：解决访问模式 4 (`getProductInventoryByProductId`)**

导入 [AnOnlineShop\$15.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_5.json) 以解决访问模式 4（`getProductInventoryByProductId`）。`warehouseItem` 实体用于跟踪每个仓库中的商品数量。在仓库中添加或移除商品时，通常会更新此项目。从 ERD 中可以看出，`product` 和 `warehouse` 之间存在多对多关系。在此处，从 `product` 到 `warehouse` 的一对多关系建模为 `warehouseItem`。稍后，也将对从 `warehouse` 到 `product` 的一对多关系进行建模。

访问模式 4 可以通过查询 `PK=ProductId` 和 `SK begins_with “w#“` 来解决。

有关 `begins_with()` 以及其他可应用于排序键的表达式的更多信息，请参阅[键条件表达式](Query.KeyConditionExpressions.md)。

**基表：**

![\[这一表设计用于查询 ProductID 和 warehouseId，来跟踪给定仓库中的产品库存。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-3-Step4.png)


**步骤 5：解决访问模式 5（`getOrderDetailsByOrderId`）和 6（`getProductByOrderId`）**

通过导入 [AnOnlineShop\$16.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_6.json)，向表中添加更多的 `customer`、`product` 和 `warehouse` 项目。然后，导入 [AnOnlineShop\$17.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_7.json)，为 `order` 构建一个项目集合，该集合可以处理访问模式 5（`getOrderDetailsByOrderId`）和 6（`getProductByOrderId`）。您可以看到建模为 orderItem 实体的 `order` 和 `product` 之间的一对多关系。

要解决访问模式 5（`getOrderDetailsByOrderId`），请使用 `PK=orderId` 查询表。这将提供有关订单的所有信息，包括 `customerId` 和订购的商品。

**基表：**

![\[这一表设计使用 orderId 进行查询，来获取有关所有已订购产品的信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-4-Step5.png)


要解决访问模式 6（`getProductByOrderId`），我们只需要读取 `order` 中的商品。使用 `PK=orderId` 和 `SK begins_with “p#”` 查询表来实现这一目标。

**基表：**

![\[这一表设计使用 orderId 和 productId 进行查询，来获取订单中的产品。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-5-Step5.png)


**步骤 6：解决访问模式 7（`getInvoiceByOrderId`）**

导入 [AnOnlineShop\$18.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_8.json)，将 `invoice` 实体添加到*订单* 项目集合中，以处理访问模式 7（`getInvoiceByOrderId`）。为了解决这种访问模式，您可以将查询操作与 `PK=orderId` 和 `SK begins_with “i#”` 结合使用。

**基表：**

![\[这一表设计在订单项目集合中使用发票实体，来按 orderId 获取发票。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-6-Step6.png)


**步骤 7：解决访问模式 8（`getShipmentByOrderId`)**

导入 [AnOnlineShop\$19.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_9.json)，将 `shipment` 实体添加到*订单* 项目集合中，以解决访问模式 8（`getShipmentByOrderId`）。我们通过在单表设计中添加更多类型的实体来扩展相同的垂直分区模型。请注意*订单* 项目集合如何包含 `order` 实体与 `shipment`、`orderItem` 和 `invoice` 实体之间的不同关系。

要按 `orderId` 获取货件，可以使用 `PK=orderId` 和 `SK begins_with “sh#”` 执行查询操作。

**基表：**

![\[这一表设计将货件实体添加到订单项目集合中，来按订单 ID 获取货件。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-7-Step7.png)


**步骤 8：解决访问模式 9（`getOrderByProductIdForDateRange`)**

我们在上一步中创建了一个*订单* 项目集合。此访问模式具有新的查找维度（`ProductID` 和 `Date`），这要求您扫描整个表并筛选掉相关记录以获取目标项目。为了解决这种访问模式，我们需要创建[全局二级索引（GSI）](GSI.md)。导入 [AnOnlineShop\$110.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_10.json) 以使用 GSI 创建新的项目集合，从而可以从多个*订单* 项目集合中检索 `orderItem` 数据。数据现在具有 `GSI1-PK` 和 `GSI1-SK`，它们将分别是 `GSI1` 的分区键和排序键。

DynamoDB 会自动将包含 GSI 的键属性的项目从表填充到 GSI。无需手动在 GSI 中进行任何其他插入。

要解决访问模式 9，请使用 `GSI1-PK=productId` 和 `GSI1SK between (date1, date2)` 对 `GSI1` 执行查询。

**基表：**

![\[使用 GSI 的表设计，用于从多个订单项目集合中获取订单数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-8-Step8-Base.png)


**GSI1：**

![\[此 GSI 设计以 ProductID 和 Date 作为分区键和排序键，来按产品 ID 和日期获取订单。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-9-Step8-GSI.png)


**步骤 9：解决访问模式 10（`getInvoiceByInvoiceId`）和 11（`getPaymentByInvoiceId`）**

导入 [AnOnlineShop\$111.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_11.json) 以解决访问模式问题 10（`getInvoiceByInvoiceId`）和 11（`getPaymentByInvoiceId`），这两者都与 `invoice` 相关。尽管这些是两种不同的访问模式，但它们是使用相同的键条件实现的。`Payments` 定义为 `invoice` 实体上具有映射数据类型的属性。

**注意**  
`GSI1-PK` 和 `GSI1-SK` 已重载以存储有关不同实体的信息，因此，可以从同一 GSI 提供多种访问模式。有关 GSI 重载的更多信息，请参阅[在 DynamoDB 中重载全局二级索引](bp-gsi-overloading.md)。

要解决访问模式 10 和 11，请使用 `GSI1-PK=invoiceId` 和 `GSI1-SK=invoiceId` 查询 `GSI1`。

**GSI1：**

![\[此 GSI 设计以 invoiceId 作为分区键和排序键，来通过发票 ID 获取发票和付款。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-10-Step9.png)


**步骤 10：解决访问模式 12（`getShipmentDetailsByShipmentId`）和 13（`getShipmentByWarehouseId`）**

导入 [AnOnlineShop\$112.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_12.json) 以解决访问模式问题 12（`getShipmentDetailsByShipmentId`）和 13（`getShipmentByWarehouseId`）。

请注意，`shipmentItem` 实体添加到基表上的*订单* 项目集合中，以便能够在单个查询操作中检索有关订单的所有详细信息。

**基表：**

![\[这一表设计在订单项目集合中使用 shipmentItem 实体，来获取所有订单详细信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-11-Step10.png)


`GSI1` 分区和排序键已用于对 `shipment` 和 `shipmentItem` 之间的一对多关系进行建模。要解决访问模式 12（`getShipmentDetailsByShipmentId`），请使用 `GSI1-PK=shipmentId` 和 `GSI1-SK=shipmentId` 查询 `GSI1`。

**GSI1：**

![\[此 GSI1 设计以 shipmentId 作为分区键和排序键，来按货件 ID 获取货件详细信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-12-Step10-GSI.png)


我们需要创建另一个 GSI（`GSI2`），来为访问模式 13（`getShipmentByWarehouseId`）的 `warehouse` 和 `shipment` 之间新的一对多关系建模。要解决此访问模式，请使用 `GSI2-PK=warehouseId` 和 `GSI2-SK begins_with “sh#”` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 warehouseId 和 shipmentId 作为分区键和排序键，以便按仓库获取货件。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-13-Step10-GSI2.png)


**步骤 11：解决访问模式 14（`getProductInventoryByWarehouseId`）、15（`getInvoiceByCustomerIdForDateRange`）和 16（`getProductsByCustomerIdForDateRange`）**

导入 [AnOnlineShop\$113.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_13.json)，以添加与下一组访问模式相关的数据。要解决访问模式 14（`getProductInventoryByWarehouseId`），请使用 `GSI2-PK=warehouseId` 和 `GSI2-SK begins_with “p#”` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 warehouseId 和 productId 作为分区键和排序键，来解决访问模式 14。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-14-Step11-GSI2.png)


要解决访问模式 15（`getInvoiceByCustomerIdForDateRange`），请使用 `GSI2-PK=customerId` 和 `GSI2-SK between (i#date1, i#date2)` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 customerId 和发票日期范围作为分区键和排序键，来解决访问模式 15。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-15-Step11-GSI2.png)


要解决访问模式 16（`getProductsByCustomerIdForDateRange`），请使用 `GSI2-PK=customerId` 和 `GSI2-SK between (p#date1, p#date2)` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 customerId 和发票日期范围作为分区键和排序键，来解决访问模式 16。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-16-Step11-GSI2.png)


**注意**  
在 [NoSQL Workbench](workbench.md) 中，*分面* 表示应用程序对 DynamoDB 的不同数据访问模式。分面为您提供了一种查看表中数据子集的方法，而不必查看不符合分面约束的记录。分面是一种可视化数据建模工具，在 DynamoDB 中不作为可用的构造存在，因为它们纯粹是对访问模式建模的帮助。  
导入 [AnOnlineShop\$1facets.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_facets.json)，以查看此使用案例的各个分面。

下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 
| --- | --- | --- | --- | --- | 
| getCustomerByCustomerId | 基表 | GetItem |  PK=customerId | SK=customerId | 
| getProductByProductId | 基表 | GetItem |  PK=productId | SK=productId | 
| getWarehouseByWarehouseId | 基表 | GetItem |  PK=warehouseId | SK=warehouseId | 
| getProductInventoryByProductId | 基表 | Query |  PK=productId | SK begins\$1with "w\$1" | 
| getOrderDetailsByOrderId | 基表 | Query |  PK=orderId |  | 
| getProductByOrderId | 基表 | Query |  PK=orderId | SK begins\$1with "p\$1" | 
| getInvoiceByOrderId |  基表 | Query |  PK=orderId | SK begins\$1with "i\$1" | 
| getShipmentByOrderId |  基表 | Query |  PK=orderId | SK begins\$1with "sh\$1" | 
| getOrderByProductIdForDateRange |  GSI1 | 查询 |  PK=productId | date1 和 date2 之间的 SK | 
| getInvoiceByInvoiceId |  GSI1 | 查询 |  PK=invoiceId | SK=invoiceId | 
| getPaymentByInvoiceId |  GSI1 | 查询 |  PK=invoiceId | SK=invoiceId | 
| getShipmentDetailsByShipmentId |  GSI1 | 查询 |  PK=shipmentId | SK=shipmentId | 
| getShipmentByWarehouseId |  GSI2 | 查询 |  PK=warehouseId | SK begins\$1with "sh\$1" | 
| getProductInventoryByWarehouseId |  GSI2 | 查询 |  PK=warehouseId | SK begins\$1with "p\$1" | 
| getInvoiceByCustomerIdForDateRange |  GSI2 | 查询 |  PK=customerId | i\$1date1 和 i\$1date2 之间的 SK | 
| getProductsByCustomerIdForDateRange |  GSI2 | 查询 |  PK=customerId | p\$1date1 和 p\$1date2 之间的 SK | 

### 在线商店最终架构
<a name="data-modeling-schema-online-store-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 设计模式](https://github.com/aws-samples/aws-dynamodb-examples/tree/master/schema_design/SchemaExamples)。

**基表**：

![\[带有属性（例如 EntityName 和 Name）的在线商店基表的最终架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-17-Final-BaseTable.png)


**GSI1**

![\[带有属性（例如 EntityType）的在线商店基表的最终 GSI1 架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-18-Final-GSI1.png)


**GSI2**

![\[带有属性（例如 EntityType）的在线商店基表的最终 GSI2 架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-19-Final-GSI2.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-online-shop-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。

# 在 DynamoDB 中建模关系数据的最佳实践
<a name="bp-relational-modeling"></a>

本节提供了在 Amazon DynamoDB 中对关系数据建模的最佳实践。首先，我们介绍传统的数据建模概念。然后，我们将介绍使用 DynamoDB 相对于传统关系数据库管理系统的优势 – 它如何消除对 JOIN 操作的需求并减少开销。

然后，我们将解释如何设计可高效扩展的 DynamoDB 表。最后，我们提供一个如何在 DynamoDB 中对关系数据进行建模的示例。

**Topics**
+ [传统的关系数据库模型](#SQLtoNoSQL.relational-modeling2)
+ [DynamoDB 如何消除对 JOIN 操作的需求](#bp-relational-modeling-joins)
+ [DynamoDB 事务如何消除写入进程的开销](#bp-relational-modeling-transactions)
+ [在 DynamoDB 中为关系数据建模的初始步骤](bp-modeling-nosql.md)
+ [在 DynamoDB 中为关系数据建模的示例](bp-modeling-nosql-B.md)

## 传统的关系数据库模型
<a name="SQLtoNoSQL.relational-modeling2"></a>

传统关系数据库管理系统（RDBMS）在规范化关系结构中存储数据。关系数据模型的目标是（通过规范化）减少数据重复，支持引用完整性并减少数据异常。

以下模式是通用订单输入应用程序的关系数据模型示例。该应用程序支持一种人力资源模式，此模式为理论制造商的运营和业务支持系统提供大力支持。

![\[示例 RDBMS 架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/RDBMS.png)


作为一种非关系数据库服务，与传统的关系数据库管理系统相比，DynamoDB 具有许多优势。

## DynamoDB 如何消除对 JOIN 操作的需求
<a name="bp-relational-modeling-joins"></a>

RDBMS 使用结构查询语言（SQL）将数据返回到应用程序。由于数据模型的规范化，此类查询通常需要使用 `JOIN` 运算符来合并来自一个或多个表的数据。

例如，要生成按照可发运每个项目的所有仓库库存数量排序的采购订单项目列表，可以对上述架构发出下面的 SQL 查询。

```
SELECT * FROM Orders
  INNER JOIN Order_Items ON Orders.Order_ID = Order_Items.Order_ID
  INNER JOIN Products ON Products.Product_ID = Order_Items.Product_ID
  INNER JOIN Inventories ON Products.Product_ID = Inventories.Product_ID
  ORDER BY Quantity_on_Hand DESC
```

这种 SQL 查询可提供用于访问数据的灵活 API，但需要大量处理。查询中的每个联接都会增加查询的运行时复杂性，因为每个表的数据都必须暂存，然后汇集才能返回结果集。

可能影响查询运行时间的其它因素包括表的大小以及所联接的列是否有索引。上述查询对多个表发起复杂查询，然后对结果集进行排序。

消除对 `JOINs` 的需求是 NoSQL 数据建模的核心所在。这就是我们构建 DynamoDB 来支持 Amazon.com 的原因，也是 DynamoDB 在任何规模都能提供一致性能的原因。考虑到 SQL 查询和 `JOINs` 的运行时复杂性，RDBMS 的性能在大规模情况下并不稳定。随着客户应用程序的增长，这会导致性能问题。

虽然规范化数据确实减少了存储到磁盘的数据量，但影响性能的最受限制的资源通常是 CPU 时间和网络延迟。

DynamoDB 旨在消除 `JOINs`（并鼓励数据非规范化）和优化数据库架构，通过对某项目的单个请求来完全回答应用程序查询，从而最大限度地减少这两种限制。这些特性使 DynamoDB 能够在任何规模上提供个位数、毫秒级的性能。这是因为对于常见的访问模式，无论数据大小如何，DynamoDB 操作的运行时复杂度都是恒定的。

## DynamoDB 事务如何消除写入进程的开销
<a name="bp-relational-modeling-transactions"></a>

另一个可能减慢 RDBMS 速度的因素是利用事务写入到规范化模式。如示例所示，大多数在线事务处理 (OLTP) 应用程序使用的关系数据结构存储在 RDBMS 中时，必须分解并分布在多个逻辑表中。

因此，需要一个符合 ACID 的事务框架，避免应用程序尝试读取正在写入的对象时可能发生的争用情况和数据完整性问题。此类事务框架与关系架构相结合，会大幅增加写入进程的开销。

在 DynamoDB 中实现事务可以防止 RDBMS 中发现的常见扩展问题。为此，DynamoDB 将事务作为单个 API 调用发出，并限制该单个事务中可以访问的项目数量。长时间运行的事务可能会由于长时间或永久锁定数据而导致操作问题，因为事务从不会关闭。

为了防止在 DynamoDB 中出现此类问题，使用以下两个不同的 API 操作实现事务：`TransactWriteItems` 和 `TransactGetItems`。这些 API 操作不具有 RDBMS 中常见的开始和结束语义。此外，DynamoDB 还有一个事务内 100 个项目的访问限制，以便以类似方式防止长时间运行的事务。要了解有关 DynamoDB 事务的更多信息，请参阅[处理事务](transactions.md)。

为此，如果业务需要低延迟响应高流量查询，采用 NoSQL 系统通常具有技术和经济意义。Amazon DynamoDB 可以避免这些限制，帮助解决问题。

RDBMS 的性能因为下列原因通常无法正常扩展：
+ 使用成本高昂的连接，重新组织需要的查询结果视图。
+ 规范化数据并存储在多个表中，需要多个查询以写入磁盘。
+ 通常产生 ACID 合规事务系统的性能成本。

DynamoDB 可以正常扩展的原因包括：
+ 架构灵活性支持 DynamoDB 在单个项目内存储复杂层次数据。
+ 复合键设计支持将相关项目一起存储在同一个表。
+ 事务是在单个操作中执行的。可以访问的项目数量限制为 100，以避免长时间运行的操作。

针对数据存储的查询变得简单得多，通常采用以下形式：

```
SELECT * FROM Table_X WHERE Attribute_Y = "somevalue"
```

与前面示例中的 RDBMS 相比，DynamoDB 可以更轻松返回请求的数据。

# 在 DynamoDB 中为关系数据建模的初始步骤
<a name="bp-modeling-nosql"></a>

**注意**  
NoSQL 设计需要不同于 RDBMS 设计的思维模式。对于 RDBMS，可以创建规范化数据模型，不考虑访问模式。以后出现新问题和查询要求后进行扩展。而在 Amazon DynamoDB 中，应先了解需要解决的问题，再开始设计架构。预先了解业务问题和应用程序使用案例是至关重要的。

要开始设计能够高效扩展的 DynamoDB 表，必须先采取几个措施，确定其需要的支持的运营和业务支持系统 (OSS/BSS) 所需的访问模式。
+ 对于新应用程序，查看有关活动和目标的用户案例。记录确定的各种使用案例，然后分析这些案例需要的访问模式。
+ 对于现有应用程序，分析查询日志以了解人们目前使用该系统的方式，以及键访问模式。

完成此过程后，应获得一个可能如下所示的列表。


**订单条目应用程序的访问模式**  

| 模式编号 | 访问模式说明 | 
| --- | --- | 
| 1 | 按员工 ID 查找员工详细信息 | 
| 2 | 按员工姓名查询员工详细信息 | 
| 3 | 查找员工电话号码 | 
| 4 | 查找客户电话号码 | 
| 5 | 获取客户在日期范围内的订单 | 
| 6 | 显示日期范围内的所有未结订单 | 
| 7 | 查看最近聘用的所有员工 | 
| 8 | 查找某个仓库中的所有员工 | 
| 9 | 获取某个产品在订单上的所有项目 | 
| 10 | 获取某个产品在所有仓库中的库存 | 
| 11 | 按客户代表获取客户 | 
| 12 | 按客户代表获取订单 | 
| 13 | 获取担任某个职位的员工 | 
| 14 | 按产品和仓库获取库存 | 
| 15 | 获取总产品库存 | 

实际应用程序的列表可能更长。这个列表代表生产环境中可能出现的查询模式复杂性的范围。

DynamoDB 架构设计的现代化方法使用面向聚合的原则，根据访问模式而不是严格的实体边界对数据进行分组。此方法考虑多种设计模式：
+ *单个表设计*：使用复合排序键、重载的全局二级索引以及相邻列表模式，在一个表中存储多种实体类型
+ *多表设计*：为具有独立操作特征和低访问相关性的实体使用单独的表，使用策略性 GSI 进行跨实体查询
+ *聚合设计*：在相关的数据始终一起访问时嵌入相关数据（订单 \$1 订单项目），或者为标识关系使用项目集合（产品 \$1 库存）

您应根据具体的访问模式、数据特征和运行要求来选择这些方法。可以使用这些元素构造数据，使得应用程序可以在表或索引上使用单个查询，检索对于给定访问模式所需的任何内容。

**注意**  
究竟是选择单表设计还是多表设计应视您的特定要求而定。当多个实体具有高访问相关性和相似的操作特征时，适合使用单表设计。当实体具有独立的操作要求、不同的访问模式或需要明确的操作边界时，则首选使用多表设计。本指南中的示例演示的是多表方法，其中采用了策略性聚合和逆规范化。

要使用 NoSQL Workbench for DynamoDB 来帮助可视化您的分区键设计，请参阅[使用 NoSQL Workbench 构建数据模型](workbench.Modeler.md)。

# 在 DynamoDB 中为关系数据建模的示例
<a name="bp-modeling-nosql-B"></a>

此示例介绍如何在 Amazon DynamoDB 中为关系数据建模。该 DynamoDB 表设计对应于[关系建模](bp-relational-modeling.md)中显示的关系订单条目架构。该设计没有使用单个相邻列表，而是使用多个专用表，提供了明确的操作边界，同时利用策略性 GSI 高效地服务所有访问模式。

该设计方法使用面向聚合的原则，根据访问模式而不是严格的实体边界对数据进行分组。其中涉及到几个关键设计决策，例如为具有低访问相关性的实体使用单独的表，在相关的数据始终一起访问时嵌入相关数据，以及为标识关系使用项目集合。

下表及其随附的索引支持关系订单条目架构：

## 员工表设计
<a name="employee-table-design"></a>

员工表将员工信息的每个项目作为单个实体进行存储，针对直接查找员工进行了优化，并通过策略性 GSI 支持多种查询模式。此表演示针对具有独立操作特征和低跨实体访问相关性的实体设计单独表的原则。

该表使用简单分区键（employee\$1id），不带排序键，因为每位员工都是一个不同的实体。通过四个 GSI，可以按照不同属性高效地执行查询：
+ *EmployeeByName GSI*：使用包含所有员工属性的 INCLUDE 投影来支持按姓名检索完整的员工详细信息，使用 employee\$1id 作为排序键来处理可能会出现的重名情况。
+ *EmployeeByWarehouse GSI*：使用仅包含基本属性（name、job\$1title、hire\$1date）的 INCLUDE 投影来尽可能降低存储成本，同时支持基于仓库的查询
+ *EmployeeByJobTitle GSI*：使用 INCLUDE 投影启用基于职位的查询，用于报告和组织分析
+ *EmployeeByHireDate GSI*：使用静态分区键值“EMPLOYEE”，将 hire\$1date 作为排序键，来实现针对最近招聘员工的高效日期范围查询。由于员工信息的添加/更新通常低于 1000 WCU，因此单个分区可以处理写入负载，而不会出现热分区问题


**员工表 – 基表结构**  

| employee\$1id（PK） | name | phone\$1numbers | warehouse\$1id | job\$1title | hire\$1date | entity\$1type | 
| --- | --- | --- | --- | --- | --- | --- | 
| emp\$1001 | John Smith | ["\$11-555-0101"] | wh\$1sea | Manager | 2024-03-15 | EMPLOYEE | 
| emp\$1002 | Jane Doe | ["\$11-555-0102", "\$11-555-0103"] | wh\$1sea | Associate | 2025-01-10 | EMPLOYEE | 
| emp\$1003 | Bob Wilson | ["\$11-555-0104"] | wh\$1pdx | Associate | 2025-06-20 | EMPLOYEE | 
| emp\$1004 | Alice Brown | ["\$11-555-0105"] | wh\$1pdx | 主管 | 2023-11-05 | EMPLOYEE | 
| emp\$1005 | Charlie Davis | ["\$11-555-0106"] | wh\$1sea | Associate | 2025-12-01 | EMPLOYEE | 


**EmployeeByName GSI – 支持员工姓名查询**  

| name（GSI-PK） | employee\$1id（GSI-SK） | phone\$1numbers | warehouse\$1id | job\$1title | hire\$1date | 
| --- | --- | --- | --- | --- | --- | 
| Alice Brown | emp\$1004 | ["\$11-555-0105"] | wh\$1pdx | 主管 | 2023-11-05 | 
| Bob Wilson | emp\$1003 | ["\$11-555-0104"] | wh\$1pdx | Associate | 2025-06-20 | 
| Charlie Davis | emp\$1005 | ["\$11-555-0106"] | wh\$1sea | Associate | 2025-12-01 | 
| Jane Doe | emp\$1002 | ["\$11-555-0102", "\$11-555-0103"] | wh\$1sea | Associate | 2025-01-10 | 
| John Smith | emp\$1001 | ["\$11-555-0101"] | wh\$1sea | Manager | 2024-03-15 | 


**EmployeeByWarehouse GSI – 支持仓库查询**  

| warehouse\$1id（GSI-PK） | employee\$1id（GSI-SK） | name | job\$1title | hire\$1date | 
| --- | --- | --- | --- | --- | 
| wh\$1pdx | emp\$1003 | Bob Wilson | Associate | 2025-06-20 | 
| wh\$1pdx | emp\$1004 | Alice Brown | 主管 | 2023-11-05 | 
| wh\$1sea | emp\$1001 | John Smith | Manager | 2024-03-15 | 
| wh\$1sea | emp\$1002 | Jane Doe | Associate | 2025-01-10 | 
| wh\$1sea | emp\$1005 | Charlie Davis | Associate | 2025-12-01 | 


**EmployeeByJobTitle GSI – 支持职位查询**  

| job\$1title（GSI-PK） | employee\$1id（GSI-SK） | name | warehouse\$1id | hire\$1date | 
| --- | --- | --- | --- | --- | 
| Associate | emp\$1002 | Jane Doe | wh\$1sea | 2025-01-10 | 
| Associate | emp\$1003 | Bob Wilson | wh\$1pdx | 2025-06-20 | 
| Associate | emp\$1005 | Charlie Davis | wh\$1sea | 2025-12-01 | 
| Manager | emp\$1001 | John Smith | wh\$1sea | 2024-03-15 | 
| 主管 | emp\$1004 | Alice Brown | wh\$1pdx | 2023-11-05 | 


**EmployeeByHireDate GSI – 支持最近招聘员工查询**  

| entity\$1type（GSI-PK） | hire\$1date（GSI-SK） | employee\$1id | name | warehouse\$1id | 
| --- | --- | --- | --- | --- | 
| EMPLOYEE | 2023-11-05 | emp\$1004 | Alice Brown | wh\$1pdx | 
| EMPLOYEE | 2024-03-15 | emp\$1001 | John Smith | wh\$1sea | 
| EMPLOYEE | 2025-01-10 | emp\$1002 | Jane Doe | wh\$1sea | 
| EMPLOYEE | 2025-06-20 | emp\$1003 | Bob Wilson | wh\$1pdx | 
| EMPLOYEE | 2025-12-01 | emp\$1005 | Charlie Davis | wh\$1sea | 

## 客户表设计
<a name="customer-table-design"></a>

客户表通过对 account\$1rep\$1id 的策略性逆规范化来维护客户信息，从而实现高效的客户代表查询。这种设计选择以少量存储开销来提高查询性能，避免了联接客户数据和客户代表数据的需求。

该表使用列表属性来支持每个客户的多个电话号码，这体现了 DynamoDB 的架构灵活性。单个 GSI 可以实现客户代表工作流：
+ *CustomerByAccountRep GSI*：使用包含姓名和电子邮件属性的 INCLUDE 投影来支持客户代表的客户管理，而无需检索完整的客户记录


**客户表 – 基表结构**  

| customer\$1id（PK） | name | phone\$1numbers | 电子邮件 | account\$1rep\$1id | 
| --- | --- | --- | --- | --- | 
| cust\$1001 | Acme Corp | ["\$11-555-1001"] | contact@acme.com | rep\$1001 | 
| cust\$1002 | TechStart Inc | ["\$11-555-1002", "\$11-555-1003"] | info@techstart.com | rep\$1001 | 
| cust\$1003 | Global Traders | ["\$11-555-1004"] | sales@globaltraders.com | rep\$1002 | 
| cust\$1004 | BuildRight LLC | ["\$11-555-1005"] | orders@buildright.com | rep\$1002 | 
| cust\$1005 | FastShip Co | ["\$11-555-1006"] | support@fastship.com | rep\$1003 | 


**CustomerByAccountRep GSI – 支持客户代表查询**  

| account\$1rep\$1id（GSI-PK） | customer\$1id（GSI-SK） | name | 电子邮件 | 
| --- | --- | --- | --- | 
| rep\$1001 | cust\$1001 | Acme Corp | contact@acme.com | 
| rep\$1001 | cust\$1002 | TechStart Inc | info@techstart.com | 
| rep\$1002 | cust\$1003 | Global Traders | sales@globaltraders.com | 
| rep\$1002 | cust\$1004 | BuildRight LLC | orders@buildright.com | 
| rep\$1003 | cust\$1005 | FastShip Co | support@fastship.com | 

## 订单表设计
<a name="order-table-design"></a>

订单表使用垂直分区，为订单标题和订单项目使用单独的项目。这种设计可以按照产品高效地进行查询，同时将所有订单组成部分保持在同一个分区内，从而实现高效访问。每个订单包含多个项目：
+ *订单标题*：包含 PK=order\$1id、SK=order\$1id 的订单元数据
+ *订单项目*：PK=order\$1id、SK=product\$1id 的单独行项目，实现直接产品查询

**注意**  
这种垂直分区方法牺牲了嵌入式订单项目的简化性，换来的是查询灵活性的增强。每个订单项目都成为一个单独的 DynamoDB 项目，实现了按照产品高效地进行查询，同时将所有订单数据保存在同一个分区内，这样就能在单个请求中高效进行检索。

该表包括 account\$1rep\$1id（从客户表复制）的策略性逆规范化，无需查找客户即可直接查询客户代表。对于高吞吐量写入场景，OPEN 订单包括状态和分片属性，实现了跨多个分区的写入分片。

四个 GSI 支持不同的查询模式并具有优化的投影：
+ *OrderByCustomerDate GSI*：使用包含订单摘要和项目详细信息的 INCLUDE 投影，支持查询带有日期范围筛选的客户订单历史记录
+ *OpenOrdersByDate GSI（稀疏、分片）*：使用多属性分区键（状态 \$1 分片）和 5 个分片，将 5000 WPS（每秒写入次数）在分区之间进行分配（每个分区 1000 WPS，与 DynamoDB 的每个分区 1000 WCU 限制相匹配）。仅对 OPEN 订单（占总数的 20%）编制索引，这有助于降低 GSI 存储成本。需要在所有 5 个分片上并行执行查询，同时在客户端合并结果
+ *OrderByAccountRep GSI*：使用包含订单摘要属性的 INCLUDE 来支持客户代表工作流，而无需完整订单详细信息
+ *ProductInOrders GSI*：从 OrderItem 记录（PK=order\$1id，SK=product\$1id）创建，此 GSI 实现了查找包含某个特定产品的所有订单的查询。使用 INCLUDE 投影及订单上下文（customer\$1id、order\$1date、quantity）进行产品需求分析


**订单表 – 基表结构（垂直分区）**  

| PK | SK | customer\$1id | order\$1date | status | account\$1rep\$1id | quantity | 价格 | 分片 | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| ord\$1001 | ord\$1001 | cust\$1001 | 2025-11-15 | 已关闭 | rep\$1001 |  |  |  | 
| ord\$1001 | prod\$1100 |  |  |  |  | 5 | 25.00 |  | 
| ord\$1002 | ord\$1002 | cust\$1001 | 2025-12-20 | OPEN | rep\$1001 |  |  | 0 | 
| ord\$1002 | prod\$1101 |  |  |  |  | 10 | 15.00 |  | 
| ord\$1003 | ord\$1003 | cust\$1002 | 2026-01-05 | OPEN | rep\$1001 |  |  | 2 | 
| ord\$1003 | prod\$1100 |  |  |  |  | 3 | 25.00 |  | 


**OrderByCustomerDate GSI – 支持客户订单查询**  

| customer\$1id（GSI-PK） | order\$1date（GSI-SK） | order\$1id | status | total\$1amount | order\$1items | 分片 | 
| --- | --- | --- | --- | --- | --- | --- | 
| cust\$1001 | 2025-11-15 | ord\$1001 | 已关闭 | 225.00 | [\$1product\$1id: "prod\$1100", qty: 5\$1] |  | 
| cust\$1001 | 2025-12-20 | ord\$1002 | OPEN | 150.00 | [\$1product\$1id: "prod\$1101", qty: 10\$1] | 0 | 
| cust\$1002 | 2026-01-05 | ord\$1003 | OPEN | 175.00 | [\$1product\$1id: "prod\$1100", qty: 3\$1] | 2 | 
| cust\$1003 | 2025-10-10 | ord\$1004 | 已关闭 | 250.00 | [\$1product\$1id: "prod\$1101", qty: 5\$1] |  | 
| cust\$1004 | 2026-01-03 | ord\$1005 | OPEN | 200.00 | [\$1product\$1id: "prod\$1100", qty: 20\$1] | 1 | 


**OpenOrdersByDate GSI（稀疏、分片）– 支持高吞吐量的未结订单查询**  

| status（GSI-PK-1） | shard（GSI-PK-2） | order\$1date（SK） | order\$1id | customer\$1id | account\$1rep\$1id | order\$1items | total\$1amount | 
| --- | --- | --- | --- | --- | --- | --- | --- | 
| OPEN | 0 | 2025-12-20 | ord\$1002 | cust\$1001 | rep\$1001 | [\$1product\$1id: "prod\$1101", qty: 10\$1] | 150.00 | 
| OPEN | 1 | 2026-01-03 | ord\$1005 | cust\$1004 | rep\$1002 | [\$1product\$1id: "prod\$1100", qty: 20\$1] | 200.00 | 
| OPEN | 2 | 2026-01-05 | ord\$1003 | cust\$1002 | rep\$1001 | [\$1product\$1id: "prod\$1100", qty: 3\$1] | 175.00 | 


**OrderByAccountRep GSI – 支持客户代表订单查询**  

| account\$1rep\$1id（GSI-PK） | order\$1date（GSI-SK） | order\$1id | customer\$1id | status | total\$1amount | 
| --- | --- | --- | --- | --- | --- | 
| rep\$1001 | 2025-11-15 | ord\$1001 | cust\$1001 | 已关闭 | 225.00 | 
| rep\$1001 | 2025-12-20 | ord\$1002 | cust\$1001 | OPEN | 150.00 | 
| rep\$1001 | 2026-01-05 | ord\$1003 | cust\$1002 | OPEN | 175.00 | 
| rep\$1002 | 2025-10-10 | ord\$1004 | cust\$1003 | 已关闭 | 250.00 | 
| rep\$1002 | 2026-01-03 | ord\$1005 | cust\$1004 | OPEN | 200.00 | 


**ProductInOrders GSI – 支持产品订单查询**  

| product\$1id（GSI-PK） | order\$1id（GSI-SK） | customer\$1id | order\$1date | quantity | 
| --- | --- | --- | --- | --- | 
| prod\$1100 | ord\$1001 | cust\$1001 | 2025-11-15 | 5 | 
| prod\$1100 | ord\$1003 | cust\$1002 | 2026-01-05 | 3 | 
| prod\$1101 | ord\$1002 | cust\$1001 | 2025-12-20 | 10 | 

## 产品表设计
<a name="product-table-design"></a>

产品表使用项目集合模式，将产品元数据和库存数据存储在同一个分区中。这种设计利用了商品与库存之间的标识关系，即没有父产品时就不会有库存。使用 PK=product\$1id 和 SK=product\$1id 作为产品元数据，并使用 SK=warehouse\$1id 来确定库存项目，这样就无需单独的库存表和 GSI，从而将成本降低了大约 50%。

这种模式可以针对某个产品高效地查询单个仓库中的库存（使用复合键 GetItem）和所有仓库中的库存（按分区键查询）。产品元数据项目中的 total\$1inventory 属性提供逆规范化聚合，用于快速查找总库存。


**产品表 – 基表结构（项目集合模式）**  

| product\$1id（PK） | warehouse\$1id（SK） | product\$1name | category | unit\$1price | inventory\$1quantity | total\$1inventory | 
| --- | --- | --- | --- | --- | --- | --- | 
| prod\$1100 | prod\$1100 | Widget A | Hardware | 25.00 |  | 500 | 
| prod\$1100 | wh\$1sea |  |  |  | 200 |  | 
| prod\$1100 | wh\$1pdx |  |  |  | 150 |  | 
| prod\$1100 | wh\$1atl |  |  |  | 150 |  | 
| prod\$1101 | prod\$1101 | Gadget B | Electronics | 50.00 |  | 300 | 
| prod\$1101 | wh\$1sea |  |  |  | 100 |  | 
| prod\$1101 | wh\$1pdx |  |  |  | 200 |  | 

每个表都设计有特定的全局二级索引（GSI），用于高效地支持所需的访问模式。该设计使用面向聚合的原则和策略性逆规范化以及稀疏索引，来优化性能和成本。

主要设计优化包括：
+ *稀疏 GSI*：OpenOrdersByDate 仅对 OPEN 订单（占总数的 20%）编制索引，这有助于降低 GSI 存储成本
+ *项目集合模式*：产品表使用 PK=product\$1id、SK=warehouse\$1id 来存储库存，这样就无需使用单独的库存表
+ *Order \$1 OrderItems 聚合*：由于 100% 的访问相关性，作为单个项目嵌入
+ *策略性逆规范化*：订单表中复制了 account\$1rep\$1id 来实现高效查询

最后，您可以再次访问之前定义的访问模式。下表显示如何使用带有策略性 GSI 的多表设计，来高效支持各种访问模式。各模式会使用直接键查找或单个 GSI 查询，从而避免了昂贵的扫描操作，并可在任意规模下提供一致的性能。


| 序列号 | 访问模式 | 查询条件 | 
| --- | --- | --- | 
|  1  |  按员工 ID 查找员工详细信息  |  员工表：GetItem(employee\$1id="emp\$1001")  | 
|  2  |  按员工姓名查询员工详细信息  |  EmployeeByName GSI：Query(name="John Smith")  | 
|  3  |  查找员工电话号码  |  员工表：GetItem(employee\$1id="emp\$1001")  | 
|  4  |  查找客户电话号码  |  客户表：GetItem(customer\$1id="cust\$1001")  | 
|  5  |  获取客户在日期范围内的订单  |  OrderByCustomerDate GSI：Query(customer\$1id="cust\$1001", order\$1date BETWEEN "2025-01-01" AND "2025-12-31")  | 
|  6  |  显示日期范围内的所有未结订单  |  OpenOrdersByDate GSI：使用多属性 PK（status="OPEN" \$1 shard=0-4）、SK=order\$1date BETWEEN "2025-01-01" AND "2025-12-31" 并行查询 5 个分片，合并结果  | 
|  7  |  查看最近聘用的所有员工  |  EmployeeByHireDate GSI：Query(entity\$1type="EMPLOYEE", hire\$1date >= "2025-01-01")  | 
|  8  |  查找某个仓库中的所有员工  |  EmployeeByWarehouse GSI：Query(warehouse\$1id="wh\$1sea")  | 
|  9  |  获取某个产品在订单上的所有项目  |  ProductInOrders GSI：Query(product\$1id="prod\$1100")  | 
|  10  |  获取某个产品在所有仓库中的库存  |  产品表：Query(product\$1id="prod\$1100")  | 
|  11  |  按客户代表获取客户  |  CustomerByAccountRep GSI：Query(account\$1rep\$1id="rep\$1001")  | 
|  12  |  按客户代表获取订单  |  OrderByAccountRep GSI：Query(account\$1rep\$1id="rep\$1001")  | 
|  13  |  获取担任某个职位的员工  |  EmployeeByJobTitle GSI：Query(job\$1title="Manager")  | 
|  14  |  按产品和仓库获取库存  |  产品表：GetItem(product\$1id="prod\$1100", warehouse\$1id="wh\$1sea")  | 
|  15  |  获取总产品库存  |  产品表：GetItem(product\$1id="prod\$1100", warehouse\$1id="prod\$1100")  | 