在 DynamoDB 中为关系数据建模的示例 - Amazon DynamoDB
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

在 DynamoDB 中为关系数据建模的示例

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

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

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

员工表设计

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

该表使用简单分区键(employee_id),不带排序键,因为每位员工都是一个不同的实体。通过四个 GSI,可以按照不同属性高效地执行查询:

  • EmployeeByName GSI:使用包含所有员工属性的 INCLUDE 投影来支持按姓名检索完整的员工详细信息,使用 employee_id 作为排序键来处理可能会出现的重名情况。

  • EmployeeByWarehouse GSI:使用仅包含基本属性(name、job_title、hire_date)的 INCLUDE 投影来尽可能降低存储成本,同时支持基于仓库的查询

  • EmployeeByJobTitle GSI:使用 INCLUDE 投影启用基于职位的查询,用于报告和组织分析

  • EmployeeByHireDate GSI:使用静态分区键值“EMPLOYEE”,将 hire_date 作为排序键,来实现针对最近招聘员工的高效日期范围查询。由于员工信息的添加/更新通常低于 1000 WCU,因此单个分区可以处理写入负载,而不会出现热分区问题

员工表 – 基表结构
employee_id(PK) name phone_numbers warehouse_id job_title hire_date entity_type
emp_001 John Smith ["+1-555-0101"] wh_sea Manager 2024-03-15 EMPLOYEE
emp_002 Jane Doe ["+1-555-0102", "+1-555-0103"] wh_sea Associate 2025-01-10 EMPLOYEE
emp_003 Bob Wilson ["+1-555-0104"] wh_pdx Associate 2025-06-20 EMPLOYEE
emp_004 Alice Brown ["+1-555-0105"] wh_pdx 主管 2023-11-05 EMPLOYEE
emp_005 Charlie Davis ["+1-555-0106"] wh_sea Associate 2025-12-01 EMPLOYEE
EmployeeByName GSI – 支持员工姓名查询
name(GSI-PK) employee_id(GSI-SK) phone_numbers warehouse_id job_title hire_date
Alice Brown emp_004 ["+1-555-0105"] wh_pdx 主管 2023-11-05
Bob Wilson emp_003 ["+1-555-0104"] wh_pdx Associate 2025-06-20
Charlie Davis emp_005 ["+1-555-0106"] wh_sea Associate 2025-12-01
Jane Doe emp_002 ["+1-555-0102", "+1-555-0103"] wh_sea Associate 2025-01-10
John Smith emp_001 ["+1-555-0101"] wh_sea Manager 2024-03-15
EmployeeByWarehouse GSI – 支持仓库查询
warehouse_id(GSI-PK) employee_id(GSI-SK) name job_title hire_date
wh_pdx emp_003 Bob Wilson Associate 2025-06-20
wh_pdx emp_004 Alice Brown 主管 2023-11-05
wh_sea emp_001 John Smith Manager 2024-03-15
wh_sea emp_002 Jane Doe Associate 2025-01-10
wh_sea emp_005 Charlie Davis Associate 2025-12-01
EmployeeByJobTitle GSI – 支持职位查询
job_title(GSI-PK) employee_id(GSI-SK) name warehouse_id hire_date
Associate emp_002 Jane Doe wh_sea 2025-01-10
Associate emp_003 Bob Wilson wh_pdx 2025-06-20
Associate emp_005 Charlie Davis wh_sea 2025-12-01
Manager emp_001 John Smith wh_sea 2024-03-15
主管 emp_004 Alice Brown wh_pdx 2023-11-05
EmployeeByHireDate GSI – 支持最近招聘员工查询
entity_type(GSI-PK) hire_date(GSI-SK) employee_id name warehouse_id
EMPLOYEE 2023-11-05 emp_004 Alice Brown wh_pdx
EMPLOYEE 2024-03-15 emp_001 John Smith wh_sea
EMPLOYEE 2025-01-10 emp_002 Jane Doe wh_sea
EMPLOYEE 2025-06-20 emp_003 Bob Wilson wh_pdx
EMPLOYEE 2025-12-01 emp_005 Charlie Davis wh_sea

客户表设计

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

该表使用列表属性来支持每个客户的多个电话号码,这体现了 DynamoDB 的架构灵活性。单个 GSI 可以实现客户代表工作流:

  • CustomerByAccountRep GSI:使用包含姓名和电子邮件属性的 INCLUDE 投影来支持客户代表的客户管理,而无需检索完整的客户记录

客户表 – 基表结构
customer_id(PK) name phone_numbers 电子邮件 account_rep_id
cust_001 Acme Corp ["+1-555-1001"] contact@acme.com rep_001
cust_002 TechStart Inc ["+1-555-1002", "+1-555-1003"] info@techstart.com rep_001
cust_003 Global Traders ["+1-555-1004"] sales@globaltraders.com rep_002
cust_004 BuildRight LLC ["+1-555-1005"] orders@buildright.com rep_002
cust_005 FastShip Co ["+1-555-1006"] support@fastship.com rep_003
CustomerByAccountRep GSI – 支持客户代表查询
account_rep_id(GSI-PK) customer_id(GSI-SK) name 电子邮件
rep_001 cust_001 Acme Corp contact@acme.com
rep_001 cust_002 TechStart Inc info@techstart.com
rep_002 cust_003 Global Traders sales@globaltraders.com
rep_002 cust_004 BuildRight LLC orders@buildright.com
rep_003 cust_005 FastShip Co support@fastship.com

订单表设计

订单表使用垂直分区,为订单标题和订单项目使用单独的项目。这种设计可以按照产品高效地进行查询,同时将所有订单组成部分保持在同一个分区内,从而实现高效访问。每个订单包含多个项目:

  • 订单标题:包含 PK=order_id、SK=order_id 的订单元数据

  • 订单项目:PK=order_id、SK=product_id 的单独行项目,实现直接产品查询

注意

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

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

四个 GSI 支持不同的查询模式并具有优化的投影:

  • OrderByCustomerDate GSI:使用包含订单摘要和项目详细信息的 INCLUDE 投影,支持查询带有日期范围筛选的客户订单历史记录

  • OpenOrdersByDate GSI(稀疏、分片):使用多属性分区键(状态 + 分片)和 5 个分片,将 5000 WPS(每秒写入次数)在分区之间进行分配(每个分区 1000 WPS,与 DynamoDB 的每个分区 1000 WCU 限制相匹配)。仅对 OPEN 订单(占总数的 20%)编制索引,这有助于降低 GSI 存储成本。需要在所有 5 个分片上并行执行查询,同时在客户端合并结果

  • OrderByAccountRep GSI:使用包含订单摘要属性的 INCLUDE 来支持客户代表工作流,而无需完整订单详细信息

  • ProductInOrders GSI:从 OrderItem 记录(PK=order_id,SK=product_id)创建,此 GSI 实现了查找包含某个特定产品的所有订单的查询。使用 INCLUDE 投影及订单上下文(customer_id、order_date、quantity)进行产品需求分析

订单表 – 基表结构(垂直分区)
PK SK customer_id order_date status account_rep_id quantity 价格 分片
ord_001 ord_001 cust_001 2025-11-15 已关闭 rep_001
ord_001 prod_100 5 25.00
ord_002 ord_002 cust_001 2025-12-20 OPEN rep_001 0
ord_002 prod_101 10 15.00
ord_003 ord_003 cust_002 2026-01-05 OPEN rep_001 2
ord_003 prod_100 3 25.00
OrderByCustomerDate GSI – 支持客户订单查询
customer_id(GSI-PK) order_date(GSI-SK) order_id status total_amount order_items 分片
cust_001 2025-11-15 ord_001 已关闭 225.00 [{product_id: "prod_100", qty: 5}]
cust_001 2025-12-20 ord_002 OPEN 150.00 [{product_id: "prod_101", qty: 10}] 0
cust_002 2026-01-05 ord_003 OPEN 175.00 [{product_id: "prod_100", qty: 3}] 2
cust_003 2025-10-10 ord_004 已关闭 250.00 [{product_id: "prod_101", qty: 5}]
cust_004 2026-01-03 ord_005 OPEN 200.00 [{product_id: "prod_100", qty: 20}] 1
OpenOrdersByDate GSI(稀疏、分片)– 支持高吞吐量的未结订单查询
status(GSI-PK-1) shard(GSI-PK-2) order_date(SK) order_id customer_id account_rep_id order_items total_amount
OPEN 0 2025-12-20 ord_002 cust_001 rep_001 [{product_id: "prod_101", qty: 10}] 150.00
OPEN 1 2026-01-03 ord_005 cust_004 rep_002 [{product_id: "prod_100", qty: 20}] 200.00
OPEN 2 2026-01-05 ord_003 cust_002 rep_001 [{product_id: "prod_100", qty: 3}] 175.00
OrderByAccountRep GSI – 支持客户代表订单查询
account_rep_id(GSI-PK) order_date(GSI-SK) order_id customer_id status total_amount
rep_001 2025-11-15 ord_001 cust_001 已关闭 225.00
rep_001 2025-12-20 ord_002 cust_001 OPEN 150.00
rep_001 2026-01-05 ord_003 cust_002 OPEN 175.00
rep_002 2025-10-10 ord_004 cust_003 已关闭 250.00
rep_002 2026-01-03 ord_005 cust_004 OPEN 200.00
ProductInOrders GSI – 支持产品订单查询
product_id(GSI-PK) order_id(GSI-SK) customer_id order_date quantity
prod_100 ord_001 cust_001 2025-11-15 5
prod_100 ord_003 cust_002 2026-01-05 3
prod_101 ord_002 cust_001 2025-12-20 10

产品表设计

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

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

产品表 – 基表结构(项目集合模式)
product_id(PK) warehouse_id(SK) product_name category unit_price inventory_quantity total_inventory
prod_100 prod_100 Widget A Hardware 25.00 500
prod_100 wh_sea 200
prod_100 wh_pdx 150
prod_100 wh_atl 150
prod_101 prod_101 Gadget B Electronics 50.00 300
prod_101 wh_sea 100
prod_101 wh_pdx 200

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

主要设计优化包括:

  • 稀疏 GSI:OpenOrdersByDate 仅对 OPEN 订单(占总数的 20%)编制索引,这有助于降低 GSI 存储成本

  • 项目集合模式:产品表使用 PK=product_id、SK=warehouse_id 来存储库存,这样就无需使用单独的库存表

  • Order + OrderItems 聚合:由于 100% 的访问相关性,作为单个项目嵌入

  • 策略性逆规范化:订单表中复制了 account_rep_id 来实现高效查询

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

序列号 访问模式 查询条件

1

按员工 ID 查找员工详细信息

员工表:GetItem(employee_id="emp_001")

2

按员工姓名查询员工详细信息

EmployeeByName GSI:Query(name="John Smith")

3

查找员工电话号码

员工表:GetItem(employee_id="emp_001")

4

查找客户电话号码

客户表:GetItem(customer_id="cust_001")

5

获取客户在日期范围内的订单

OrderByCustomerDate GSI:Query(customer_id="cust_001", order_date BETWEEN "2025-01-01" AND "2025-12-31")

6

显示日期范围内的所有未结订单

OpenOrdersByDate GSI:使用多属性 PK(status="OPEN" + shard=0-4)、SK=order_date BETWEEN "2025-01-01" AND "2025-12-31" 并行查询 5 个分片,合并结果

7

查看最近聘用的所有员工

EmployeeByHireDate GSI:Query(entity_type="EMPLOYEE", hire_date >= "2025-01-01")

8

查找某个仓库中的所有员工

EmployeeByWarehouse GSI:Query(warehouse_id="wh_sea")

9

获取某个产品在订单上的所有项目

ProductInOrders GSI:Query(product_id="prod_100")

10

获取某个产品在所有仓库中的库存

产品表:Query(product_id="prod_100")

11

按客户代表获取客户

CustomerByAccountRep GSI:Query(account_rep_id="rep_001")

12

按客户代表获取订单

OrderByAccountRep GSI:Query(account_rep_id="rep_001")

13

获取担任某个职位的员工

EmployeeByJobTitle GSI:Query(job_title="Manager")

14

按产品和仓库获取库存

产品表:GetItem(product_id="prod_100", warehouse_id="wh_sea")

15

获取总产品库存

产品表:GetItem(product_id="prod_100", warehouse_id="prod_100")