进阶阅读约 2 分钟

DynamoDB 中的单表设计

从 SQL 过来,直觉是每个实体一张表:customersordersorder_items。在 DynamoDB 中这个直觉通常是错的。一张存储每一种实体、靠重载的键前缀来区分它们的表,能让你在一次 Query 中取出一个父项及其子项 —— 没有连接,没有 N+1。

从访问模式出发,而非实体

单表设计是访问模式优先的。在你选定单一键之前,写下你的应用发起的每一次读取 —— “获取某个客户的资料”、“按最新优先列出某个客户的订单”、“找出所有未结订单” —— 因为键存在的唯一目的就是服务这份清单。关系型范式化为存储而优化;DynamoDB 建模则为你早已知道会运行的查询而优化。把它们逐一列举出来,然后设计键,让每一项都成为一次 Query

思路

选用通用的键名(PKSK),并把实体类型编码进值里:

PKSKattributes
CUSTOMER#42PROFILEname, email, plan
CUSTOMER#42ORDER#2026-001total, status
CUSTOMER#42ORDER#2026-002total, status

现在一次 Query PK = "CUSTOMER#42" 就能在单次计费的读取中返回资料以及每一笔订单。SK begins_with "ORDER#" 则把范围缩小到仅订单。

直观地看,这些重载的项作为单个项集合堆叠在一个分区键之下:

Partition: CUSTOMER#42SK: PROFILESK: ORDER#2026-001SK: ORDER#2026-002One Query

对该分区的一次读取就会把这个客户及其每一笔订单一并交还给你。

重载的 GSI

同样的技巧也适用于索引。在项上放一个通用的 GSI1PK/GSI1SK,单个 GSI 便可根据每个项往这些属性里写入什么,来服务多种访问模式:

PKSKGSI1PKGSI1SK
ORDER#001METADATASTATUS#OPEN2026-01-04
ORDER#002METADATASTATUS#OPEN2026-01-05

现在 Query GSI1 WHERE GSI1PK = "STATUS#OPEN" 会按日期列出未结订单 —— 一个基表无法回答的模式。另一种实体可以用自己的含义复用 GSI1(例如 CATEGORY#books)。一个索引,多种查询。

多对多:邻接表

对于关系(一个用户属于多个团队,一个团队有多个用户),把这条边写两次,并把两个 id 互换:PK=USER#1, SK=TEAM#9PK=TEAM#9, SK=USER#1。查询任一侧都会列出另一侧 —— 这是 DynamoDB 对连接表的替代方案。

何时不该用单表

它并非免费。一张重载的表更难理解、更难演进,且对分析不友好。如果你的访问模式确实未知或不断变化,或者数据主要是分析型的,那么分表(或换一种存储)可能才是更明智的选择。当模式已知且高吞吐时,单表才会胜出。

错误形态的代价

把数据建模成分开的表,会迫使你用 Scan 或客户端连接来重新拼出一个客户,而这正是 Scan 暗坑。先对访问模式建模,再设计键,让每一项都成为一次 Query

项大小与容量计算器估算这些项每次读取的成本,并试用 DynoTable 浏览一个单表 schema,并排查看那些重载的集合。

更新于