DynamoDB 中的 Type 属性
在 SQL 中,一行所在的表_就是_它的类型——documents 表里的一行就是一个文档。而
DynamoDB 单表把所有实体混在同一套 schema 下,因此一个项目本身并不能回答「这是什么?」这个问题。
Type 属性把答案补了回来:它是每个项目上的一个普通字符串,标明该项目所代表的实体。
DynamoDB 中的 Type 属性是什么?
Type 属性是你在每个项目上打的一个普通字符串——例如 EntityType: "Document"——用来标明该项目所代表的实体。由于单表把许多实体混在同一套 schema 下,项目本身并不带内建的类型信息。Type 把它补了回来,让你的代码得以识别行、把 GSI 过滤到单一实体,并在迁移中存活下来。
- 每次写入都打上 Type。 每个项目上都有一个属性——
EntityType: "Document"——无一例外。它只花费几个字节,却能在日后帮你省下大麻烦。 - 它能在混合分区中识别实体。 一次
Query会把工作区、文档和评论一并返回;Type 让你的代码无需解析键前缀就能分辨谁是谁。 - 它为上的单实体过滤提供了支撑。 把 Type 投影进索引,你就能把一个重载索引缩窄到恰好一种实体类型。
- 它是迁移时的逃生舱。 当你导出数据以重新建模、或把某个实体拆到独立的表时,Type 就是你用来切分的那一列。
为什么混合表会丢失类型
单表设计把每个实体都存进一张表,背后用 PK、SK 这类通用键。这正是它的意义所在——一次 Query 就能把父项和它的子项一并返回。但这也意味着一个分区是异构的。
以一个 SaaS 文档协作应用为例。一个工作区分区里存放着工作区记录、它的文档,以及那些文档上的评论:
| PK | SK | attributes |
|---|---|---|
| WS#acme | META | name, plan, seats |
| WS#acme | DOC#a1#META | title, owner, wordCount |
| WS#acme | DOC#a1#CMT#0007 | author, body, createdAt |
| WS#acme | DOC#a1#CMT#0008 | author, body, createdAt |
Query PK = "WS#acme" 会在一次计费读取中把全部四个项目返回。现在你的代码拿到了一串原始项目,却没有可靠的办法说清哪个是文档、哪个是评论——除非去对 SK 做字符串匹配,而这在你的键格式一变时就会崩掉。
在每个项目上打上 Type
修复方法就是每次写入都加上一个属性,用来标明实体:
| PK | SK | EntityType | title |
|---|---|---|---|
| WS#acme | META | Workspace | — |
| WS#acme | DOC#a1#META | Document | Q3 Roadmap |
| WS#acme | DOC#a1#CMT#0007 | Comment | — |
基于 item.EntityType === "Document" 做分支是一个稳定的相等判断。而解析 SK.startsWith("DOC#") && SK.includes("#CMT#") 只是一种猜测,一旦你改动键就会失灵。Type 让你的读取逻辑与键编码解耦——这才是真正的收益。
一次读取返回三种实体类型;Type 属性把每个项目路由到正确的处理逻辑,完全无需触碰键。
把 GSI 过滤到单一实体
Type 的价值在索引上体现得最充分。假设你新增一个 GSI,以 GSI1PK = WS#acme、GSI1SK = updatedAt 为键,用来列出「这个工作区里最近改动过的所有内容,最新的排在最前」。一个重载索引会把文档_和_评论一并扫入——但一个信息流 UI 可能只想要文档。
有两种缩窄方式,而两者的区别关乎花钱:
| 方式 | 它的代价 | 何时使用 |
|---|---|---|
对 Type 使用 FilterExpression | 读取所有匹配的项目、按全部项目计费,再在读取之后丢弃不匹配的 | 结果中混入的实体很少见;追求快速上线 |
稀疏索引(GSI1PK 只写在目标实体上) | 只有你想要的那种实体才会进入索引 | 某一种实体占主导;你希望零浪费 |
在 us-east-1 的按需模式下,一次 GSI Query 返回 100 个各 2 KB 的混合项,
最终一致读大约计费 100 RCU —— 而对 EntityType 的 FilterExpression 在丢掉那些评论
之前,依然会把每一行都计量进去。一个从不索引评论的稀疏索引,则只为文档行计费。在
定价计算器里把两种形状都建模一遍。
FilterExpression 是在项目被读取之后、容量被消耗之后才运行的——AWS 明确指出过滤并不会降低读取成本(DynamoDB 开发者指南:FilterExpression)。对 Type 做过滤是诚实的,而非免费的:你为那些被丢掉的评论付了钱。
要把信息流缩窄到只剩文档,查询需要带上一个针对 Type 属性的条件。用 DynamoDB 表达式构建器来组装 FilterExpression、名称和值——它会生成 #t = :doc 占位符,让你不至于把保留字打错。
KeyConditionExpression GSI1PK = :ws
FilterExpression #t = :doc
ExpressionAttributeNames { "#t": "EntityType" }
ExpressionAttributeValues { ":ws": "WS#acme", ":doc": "Document" }
想让索引_只_承载文档、彻底省掉过滤?那就只在文档项目上写 GSI1PK——一个。没有 GSI 键的项目永远不会复制进索引,于是读取只会碰到文档。而 Type 属性正是告诉你的写入逻辑哪些项目符合条件的那个依据。
让取值保持稳定且单一
一次性选定取值,并把它当作枚举来对待。永远是 Document,不要一会儿 Doc 一会儿 document——飘忽不定的取值比没有取值更糟,因为你的相等判断会在某一种大小写下通过,却悄悄漏掉另一种。
每个项目只有一个 Type。如果某个项目感觉像是两个实体,那通常是建模气味——它应该拆成两个项目,各自处在自己的集合或排序键区间里,而不是一行身兼两职。
迁移带来的回报
在你需要之前就打上 Type,理由就是:重新建模。推荐的重建模路径是导出、转换、再导入——而 AWS 记录了批量导出到 S3,正是为这类离线重塑准备的(将 DynamoDB 导出到 S3)。
当那一天到来时,Type 就是你用来 GROUP BY 的那一列。想把评论提升到它们自己的表里,或者把导出数据重新归一化成按实体划分的文件、供分析仓库使用?你就在 EntityType 上切分这份导出。没有它,你就又得在数百万行里反向推断键了。
后续步骤
Type 属性是一份廉价的保险。在一次混合读取中识别实体、过滤一个重载的 GSI,并在重新建模时干净利落地拆分。从第一天起就在每次写入时打上它——事后往一张运行中的表补加它,意味着一次全量回填。
延伸阅读:单表设计介绍它所服务的混合分区模式,GSI vs LSI帮你选择稀疏索引背后的索引形态,以及 Query vs Scan讲清为什么 FilterExpression 永远救不了你的读取成本。
用 DynamoDB 表达式构建器基于 Type 构建过滤条件,然后试用 DynoTable去浏览一张真实的混合实体表,看看 Type 这一列如何在每个项目上对齐。