DynamoDB 中的单表设计
从 SQL 过来,直觉是每个实体一张表:customers、orders、order_items。在 DynamoDB 中这个直觉通常是错的。一张存储每一种实体、靠重载的键前缀来区分它们的表,能让你在一次 Query 中取出一个父项及其子项 —— 没有连接,没有 N+1。
从访问模式出发,而非实体
单表设计是访问模式优先的。在你选定单一键之前,写下你的应用发起的每一次读取 —— “获取某个客户的资料”、“按最新优先列出某个客户的订单”、“找出所有未结订单” —— 因为键存在的唯一目的就是服务这份清单。关系型范式化为存储而优化;DynamoDB 建模则为你早已知道会运行的查询而优化。把它们逐一列举出来,然后设计键,让每一项都成为一次 Query。
思路
选用通用的键名(PK、SK),并把实体类型编码进值里:
| PK | SK | attributes |
|---|---|---|
| CUSTOMER#42 | PROFILE | name, email, plan |
| CUSTOMER#42 | ORDER#2026-001 | total, status |
| CUSTOMER#42 | ORDER#2026-002 | total, status |
现在一次 Query PK = "CUSTOMER#42" 就能在单次计费的读取中返回资料以及每一笔订单。SK begins_with "ORDER#" 则把范围缩小到仅订单。
直观地看,这些重载的项作为单个项集合堆叠在一个分区键之下:
对该分区的一次读取就会把这个客户及其每一笔订单一并交还给你。
重载的 GSI
同样的技巧也适用于索引。在项上放一个通用的 GSI1PK/GSI1SK,单个 GSI 便可根据每个项往这些属性里写入什么,来服务多种访问模式:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| ORDER#001 | METADATA | STATUS#OPEN | 2026-01-04 |
| ORDER#002 | METADATA | STATUS#OPEN | 2026-01-05 |
现在 Query GSI1 WHERE GSI1PK = "STATUS#OPEN" 会按日期列出未结订单 —— 一个基表无法回答的模式。另一种实体可以用自己的含义复用 GSI1(例如 CATEGORY#books)。一个索引,多种查询。
多对多:邻接表
对于关系(一个用户属于多个团队,一个团队有多个用户),把这条边写两次,并把两个 id 互换:PK=USER#1, SK=TEAM#9 和 PK=TEAM#9, SK=USER#1。查询任一侧都会列出另一侧 —— 这是 DynamoDB 对连接表的替代方案。
何时不该用单表
它并非免费。一张重载的表更难理解、更难演进,且对分析不友好。如果你的访问模式确实未知或不断变化,或者数据主要是分析型的,那么分表(或换一种存储)可能才是更明智的选择。当模式已知且高吞吐时,单表才会胜出。
错误形态的代价
把数据建模成分开的表,会迫使你用 Scan 或客户端连接来重新拼出一个客户,而这正是 Scan 暗坑。先对访问模式建模,再设计键,让每一项都成为一次 Query。
用项大小与容量计算器估算这些项每次读取的成本,并试用 DynoTable 浏览一个单表 schema,并排查看那些重载的集合。