进阶阅读约 3 分钟

DynamoDB 中的 Type 属性

在 SQL 中,一行所在的表_就是_它的类型——documents 表里的一行就是一个文档。而 DynamoDB 单表把所有实体混在同一套 schema 下,因此一个项目本身并不能回答「这是什么?」这个问题。

Type 属性把答案补了回来:它是每个项目上的一个普通字符串,标明该项目所代表的实体。

DynamoDB 中的 Type 属性是什么?

Type 属性是你在每个项目上打的一个普通字符串——例如 EntityType: "Document"——用来标明该项目所代表的实体。由于单表把许多实体混在同一套 schema 下,项目本身并不带内建的类型信息。Type 把它补了回来,让你的代码得以识别行、把 GSI 过滤到单一实体,并在迁移中存活下来。

  • 每次写入都打上 Type。 每个项目上都有一个属性——EntityType: "Document"——无一例外。它只花费几个字节,却能在日后帮你省下大麻烦。
  • 它能在混合分区中识别实体。 一次 Query 会把工作区、文档和评论一并返回;Type 让你的代码无需解析键前缀就能分辨谁是谁。
  • 它为上的单实体过滤提供了支撑。 把 Type 投影进索引,你就能把一个重载索引缩窄到恰好一种实体类型。
  • 它是迁移时的逃生舱。 当你导出数据以重新建模、或把某个实体拆到独立的表时,Type 就是你用来切分的那一列。

为什么混合表会丢失类型

单表设计把每个实体都存进一张表,背后用 PKSK 这类通用键。这正是它的意义所在——一次 Query 就能把父项和它的子项一并返回。但这也意味着一个分区是异构的。

以一个 SaaS 文档协作应用为例。一个工作区分区里存放着工作区记录、它的文档,以及那些文档上的评论:

PKSKattributes
WS#acmeMETAname, plan, seats
WS#acmeDOC#a1#METAtitle, owner, wordCount
WS#acmeDOC#a1#CMT#0007author, body, createdAt
WS#acmeDOC#a1#CMT#0008author, body, createdAt

Query PK = "WS#acme" 会在一次计费读取中把全部四个项目返回。现在你的代码拿到了一串原始项目,却没有可靠的办法说清哪个是文档、哪个是评论——除非去对 SK 做字符串匹配,而这在你的键格式一变时就会崩掉。

在每个项目上打上 Type

修复方法就是每次写入都加上一个属性,用来标明实体:

PKSKEntityTypetitle
WS#acmeMETAWorkspace
WS#acmeDOC#a1#METADocumentQ3 Roadmap
WS#acmeDOC#a1#CMT#0007Comment

基于 item.EntityType === "Document" 做分支是一个稳定的相等判断。而解析 SK.startsWith("DOC#") && SK.includes("#CMT#") 只是一种猜测,一旦你改动键就会失灵。Type 让你的读取逻辑与键编码解耦——这才是真正的收益。

Query PK = 'WS#acme'混合分区EntityType: 'Workspace'EntityType: 'Document'EntityType: 'Comment' Type 路由

一次读取返回三种实体类型;Type 属性把每个项目路由到正确的处理逻辑,完全无需触碰键。

把 GSI 过滤到单一实体

Type 的价值在索引上体现得最充分。假设你新增一个 GSI,以 GSI1PK = WS#acmeGSI1SK = updatedAt 为键,用来列出「这个工作区里最近改动过的所有内容,最新的排在最前」。一个重载索引会把文档_和_评论一并扫入——但一个信息流 UI 可能只想要文档。

有两种缩窄方式,而两者的区别关乎花钱:

方式它的代价何时使用
对 Type 使用 FilterExpression读取所有匹配的项目、按全部项目计费,再在读取之后丢弃不匹配的结果中混入的实体很少见;追求快速上线
稀疏索引GSI1PK 只写在目标实体上)只有你想要的那种实体才会进入索引某一种实体占主导;你希望零浪费

us-east-1 的按需模式下,一次 GSI Query 返回 100 个各 2 KB 的混合项, 最终一致读大约计费 100 RCU —— 而对 EntityTypeFilterExpression 在丢掉那些评论 之前,依然会把每一行都计量进去。一个从不索引评论的稀疏索引,则只为文档行计费。在 定价计算器里把两种形状都建模一遍。

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 这一列如何在每个项目上对齐。

更新于