在 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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") |