在 DynamoDB 中何时不要用单表设计
单表设计是 DynamoDB 的默认建议,而它配得上:一次 Query 交回一个父项及其子项,没有连接,没有 N+1。
但它是一桩交换 —— 你用一个僵硬、不透明的模式买来读取速度。有些工作负载付不起那个价,而把一张表强加在它们头上,是它自己的暗坑。
在 DynamoDB 中什么时候不应该使用单表设计?
当你的工作负载是繁重的 OLAP 分析、少数不相关实体的普通 CRUD,或需要独立扩展和独立容错的实体时,应避免单表设计。在这些情况下,多表可读性更好、成本相当、且更具灵活性。只有当访问模式已知、相互关联且高频时,单表设计才真正胜出。
- 重分析? 别用单表。重载键对 OLAP 不友好 —— 导出到一个列式存储,在那里查询。
- 只有寥寥几个访问模式的普通 CRUD? 每个实体一张表就行,可读,在性能上不花你一分钱。
- 独立扩展或独立失败的实体? 分开的表让你各自调优、计费,并控制其爆炸半径。
- 单表仍然胜出,当你的模式是已知的、相关的、且高量的 —— 那正是它被设计来应对的情形。
知道单表到底花你什么
单表设计不是免费的;它只是把成本从读取路径移到了别的一切上。你付出的是可读性和灵活性。
一张在 PK/SK 背后容纳五种实体类型的表很难读、很难上手、也很难改。一种新访问模式可能意味着对分区里每一种项类型的一次回填。
所以问题不是“单表好不好?”而是“我的访问模式配得上那份僵硬吗?”当它们配不上时,去够多张表。
别给分析工作负载用单表
DynamoDB 是为 OLTP 而生的 —— 小的、已知的、点查和范围读取。分析是 OLAP:GROUP BY、大聚合、跨整个数据集的临时切片。两者朝相反方向拉扯。
单表设计让 OLAP 更糟,而非更好。重载键和混合的实体类型意味着一个分析作业必须先理清哪个项是哪个,然后才能去汇总任何东西 —— 与一次干净的列式扫描正相反。
从 SQL 过来,反射动作是对着活表写那个聚合。在 DynamoDB 里那是一次全 Scan —— 你为读取每一个项付费,那是满负荷的 Scan 暗坑。
修复办法不是一个更聪明的键。而是一个不同的存储。AWS 自己的指南就是把 DynamoDB 导出到 S3,并用一个像 Athena 的查询引擎跑分析。
把 OLTP 和 OLAP 放在分开的引擎上(AWS DynamoDB Developer Guide,S3DataExport.html)。
别给简单 CRUD 用单表
如果你的应用按各自的 id 读写寥寥几个不相干的实体,而你大概有三种访问模式,那么单表买不来任何东西。
拿一个小型 B2B 工具的一张 Tenants 表和一张 ApiKeys 表:
| TenantId | Name | PlanTier |
|---|---|---|
| TNT-8842 | Northwind | pro |
| KeyId | TenantId | Scope |
|---|---|---|
| KEY-01H9... | TNT-8842 | read-write |
每个查询是一次按 id 的 GetItem,或者一次对以 TenantId 建键的 GSI 的 Query。没有父项加子项的取回要去优化,所以没有什么可让一个重载分区去赢的。两张清晰的表比一张浑浊的读起来更好。
陷阱是因为单表是“最佳实践”就照搬它。最佳实践是访问模式优先。如果模式很简单,那么简单的形态就是对的形态。
拆分独立扩展或独立失败的表
单张表共享一个吞吐量面、一份备份、一个爆炸半径。当两个实体有天差地别的写入速率或持久性需求时,那份共享命运就变成一项负债。
设想一个车队跟踪系统。车辆很少变;它们的遥测数据每秒钟涌进来:
| VehicleId | Make | Model | Region |
|---|---|---|---|
| VEH-204 | Volvo | FH16 | eu-west |
| DeviceTs | VehicleId | SpeedKph | Fuel |
|---|---|---|---|
| 2026-06-23T10:00:01Z | VEH-204 | 88 | 0.61 |
两张表让你为一股洪流预置遥测、把车辆保持得又小又便宜、只在遥测上设一个 TTL,并阻止一场遥测写入风暴限流对车辆目录的读取。一张表把那一切都耦合起来了。
依照 2007 年的 Amazon Dynamo 论文,分区和可用性是头等关切 —— 独立的表给你对两者的独立控制。
在你提交之前把这个决定画出来
把工作负载过一道关:实体相关吗,模式已知且高量吗?如果不是,就用多张表。
下面是把这个判断画成一条流程 —— 从顶端开始,沿着第一条匹配的分支走:
只有右下那个叶子配得上单表;每一条别的路径都由不止一张表服务得更好。
单表 vs 多表,正面交锋
| 因素 | 单表 | 多表 |
|---|---|---|
| 相关读取 | 一次 Query,没有连接 | 客户端连接或额外往返 |
| 可读性 | 不透明的重载键 | 每张表一种实体,自带文档 |
| 新访问模式 | 往往是一次回填 | 孤立地添加一张表或一个 GSI |
| 分析 / OLAP | 不友好 —— 汇总前得先理清 | 仍要导出,但每实体更干净 |
| 独立扩展 | 共享吞吐量 + 爆炸半径 | 分别调优、计费、TTL、备份 |
| 最佳时机 | 已知、相关、高量的模式 | 不相关、演进中,或分析型 |
暗坑 + 下一步
镜像式的错误是过度纠正 —— 把真正相关、高量的实体拆成每实体一张表,并在你的应用里重建 SQL 风格的连接。那是单表所消灭的 N+1 陷阱。
挑访问模式所要的那个形态,而非教条。
当你确实要给关系建模时,倚靠正确的索引类型 —— 在你添加一个之前先看 GSI 对比 LSI。
当你确实要对一个多表模式写一个查询时,先在 DynamoDB 表达式构建器里勾勒那个 KeyConditionExpression。
那样你就能在一个全 Scan 形态打到生产之前抓住它。
然后试用 DynoTable,对着你自己的表浏览两种形态,看你的访问模式实际想要的是哪一种。