入门阅读约 2 分钟

DynamoDB 投影表达式

一个投影表达式是 DynamoDB 的 SELECT col1, col2:一个逗号分隔的属性名列表,告诉 GetItemQueryScan 只返回那些属性,而不是整个项。

DynamoDB 投影表达式会降低读取成本吗?

不会。ProjectionExpression 只裁减响应 payload,并不影响计费的读取容量。DynamoDB 从存储中读取完整的项,按其磁盘大小计量,然后在出口处丢掉你没有命名的属性。若要真正降低读取成本,请改用覆盖

  • 它修剪载荷,而非读取成本。 DynamoDB 从存储中读取(并计费)完整的项,然后在出口处丢掉你没命名的 属性。ProjectionExpression 是一个网络优化,而非容量优化。
  • 它是你取一个公开子集的方式。 命名一个调用者被允许看到的少数几个属性;其余的永不离开表。
  • 对任何可能是保留字的东西用 #name 占位符。 表达式里裸的属性名会与 DynamoDB 约 570 个保留字 冲突并使请求失败。
  • 要真正省读取,改用一个覆盖索引。 一个只投影你需要的列的 GSI,按它自己(更小的)大小被读取。

它实际省下什么

从 SQL 过来,你会假设 SELECT a, bSELECT * 扫描得少。在 DynamoDB 中那个直觉是错的。一次读取的 容量单元是按项在磁盘上的大小计算的,向上取整到下一个 4 KB ——在投影被应用之前。AWS 明确指出:一个 ProjectionExpression 不改变一个请求消耗的读取容量。[^rcu]

所以一个投影给你省两样东西,两样都真实但都在读取的下游:

  • 线缆上的字节。 一个 6 KB 的项作为两个小属性返回,是一个微小的响应。在一个返回数百个项的 Query 上, 那很快就累积起来。
  • 客户端的工作。 更少要反序列化的、更少要在内存中保留的、更少意外泄漏进一条日志或一个 API 响应的。

省的是 RCU。那就是那个坑:人们动用一个投影来削减账单,却看不到变化,于是断定 DynamoDB 坏了。 它没坏——你测错了杠杆。

投影一个公开用户资料

假设你运营一个用户目录。每个资料是一个项,设键让你能按昵称取出一个人:

PK = "PROFILE#ada"      (partition key)
SK = "PROFILE#ada"      (sort key — single-item collection)

这个项很胖。它承载账户的公开面孔,外加一堆私有和运维属性:

{
  "PK": "PROFILE#ada",
  "SK": "PROFILE#ada",
  "displayName": "Ada L.",
  "avatarUrl": "https://cdn.example.com/u/ada.png",
  "bio": "Builds things.",
  "emailAddress": "ada@example.com",
  "passwordResetToken": "…",
  "billingCustomerId": "cus_…",
  "lastLoginIp": "…",
  "internalRiskScore": 0.02
}

一张公开资料卡需要三个字段。取出整个项意味着 emailAddresslastLoginIpinternalRiskScore 会旅行到一个本不该看到它们的上下文。只命名那个公开子集:

GetItem  PK = "PROFILE#ada"  SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio

响应承载三个属性。私有的那些留在表里——不是在抵达之后被你的应用过滤掉,而是从一开始就根本没被序列化 进响应。这就是那个安全收益,也是一旦一个秘密已经越过边界就难以撤销的那一个。

你可以在 DynamoDB Expression Builder 里组装并复制这个确切的请求 ——名称、占位符,以及 SDK 调用——它会替你产出 ProjectionExpressionExpressionAttributeNames 映射。

# 占位符转义保留字

这里一个干净的投影会炸掉。DynamoDB 保留一长串词——namestatuscommentsizetimestamp, 以及数百个其他的。1 如果你正在投影的一个属性是其中之一,表达式里的裸名会被拒绝。

假设资料还有一个 status 属性("active""suspended")。这个会失败:

ProjectionExpression   displayName, status

status 是保留的。修法是一个表达式属性名——一个映射到真实名称的 # 前缀占位符:

ProjectionExpression       displayName, #s
ExpressionAttributeNames   { "#s": "status" }

同样的机制伸进嵌套属性。要从一个映射中拉出单个字段,或一个列表的一个元素,用文档路径语法 ——并给每一段都加占位符,因为它们中任何一个都可能是保留的:

ProjectionExpression       #addr.#city, tags[0]
ExpressionAttributeNames   { "#addr": "address", "#city": "city" }

一条实用规则:给一切都加占位符。你永远不必记住自己正踩在那约 570 个保留字中的哪一个上,而且表达式 两种写法读起来都一样。

何时覆盖索引胜过投影

如果你确实需要削减读取成本——而不只是载荷——杠杆是一个只投影你读取的属性的全局二级索引。一个 GSI 是数据的一份独立副本;你为它的投影选择 KEYS_ONLYINCLUDEALL2 一个 KEYS_ONLY 或窄 INCLUDE 索引每个项物理上更小,所以对它的一次 Query 按那个更小的大小计量。

那就是一个覆盖索引:查询完全从索引得到回答,不回基础表跑一趟。当一个热读取模式只需要大项里的少数几个 属性时,用它。

ProjectionExpression覆盖 GSI
削减载荷
削减读取成本**否是** —— 按索引的大小读取
额外存储被投影字段的第二份副本
额外写入成本写入传播到索引
最适合隐藏私有字段;小赢大项里少数字段的热读取

这个取舍是诚实的:索引花你的存储和写入容量来省读取容量。对一个从重项里读薄薄一片的频繁读取,值得; 要削一次性的 GetItem,不值得。参见 GSI 与 LSI了解挑选索引类型,以及 一个 GSI 读取何时可能陈旧,在你把它放到热路径之前。

陷阱与下一步

  • 别指望更小的账单。 单凭一个投影从不改变 RCU。如果数字没动,那是被记录的行为,而不是一个 bug。
  • 给保留字加占位符。 表达式里一个裸的 namestatus 会使请求失败——用 # 给它映射。
  • 投影键属性是免费的,而且常常有用——DynamoDB 便宜地返回它们,它们让你能分页或重新取出。
  • 动用一个覆盖索引只在一个热模式从大项里读少数几个字段时;先权衡写入/存储成本。

表达式构建器里构建 ProjectionExpression 及其属性名映射, 然后试试 DynoTable 对你自己的表运行这些投影,看着响应缩小。

[^rcu]: AWS DynamoDB 开发者指南,处理读取与写入操作——读取容量基于应用任何 ProjectionExpression 之前的项大小。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/


  1. AWS DynamoDB 开发者指南,DynamoDB 中的保留字https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
  2. AWS DynamoDB 开发者指南,属性投影KEYS_ONLY / INCLUDE / ALL)。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html

更新于