DynamoDB 投影表达式
一个投影表达式是 DynamoDB 的 SELECT col1, col2:一个逗号分隔的属性名列表,告诉 GetItem、
Query 或 Scan 只返回那些属性,而不是整个项。
DynamoDB 投影表达式会降低读取成本吗?
不会。ProjectionExpression 只裁减响应 payload,并不影响计费的读取容量。DynamoDB 从存储中读取完整的项,按其磁盘大小计量,然后在出口处丢掉你没有命名的属性。若要真正降低读取成本,请改用覆盖。
- 它修剪载荷,而非读取成本。 DynamoDB 从存储中读取(并计费)完整的项,然后在出口处丢掉你没命名的
属性。
ProjectionExpression是一个网络优化,而非容量优化。 - 它是你取一个公开子集的方式。 命名一个调用者被允许看到的少数几个属性;其余的永不离开表。
- 对任何可能是保留字的东西用
#name占位符。 表达式里裸的属性名会与 DynamoDB 约 570 个保留字 冲突并使请求失败。 - 要真正省读取,改用一个覆盖索引。 一个只投影你需要的列的 GSI,按它自己(更小的)大小被读取。
它实际省下什么
从 SQL 过来,你会假设 SELECT a, b 比 SELECT * 扫描得少。在 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
}一张公开资料卡需要三个字段。取出整个项意味着 emailAddress、lastLoginIp 和 internalRiskScore
会旅行到一个本不该看到它们的上下文。只命名那个公开子集:
GetItem PK = "PROFILE#ada" SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio
响应承载三个属性。私有的那些留在表里——不是在抵达之后被你的应用过滤掉,而是从一开始就根本没被序列化 进响应。这就是那个安全收益,也是一旦一个秘密已经越过边界就难以撤销的那一个。
你可以在 DynamoDB Expression Builder 里组装并复制这个确切的请求
——名称、占位符,以及 SDK 调用——它会替你产出 ProjectionExpression 和 ExpressionAttributeNames 映射。
用 # 占位符转义保留字
这里一个干净的投影会炸掉。DynamoDB 保留一长串词——name、status、comment、size、timestamp,
以及数百个其他的。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_ONLY、INCLUDE 或 ALL。2 一个 KEYS_ONLY
或窄 INCLUDE 索引每个项物理上更小,所以对它的一次 Query 按那个更小的大小计量。
那就是一个覆盖索引:查询完全从索引得到回答,不回基础表跑一趟。当一个热读取模式只需要大项里的少数几个 属性时,用它。
ProjectionExpression | 覆盖 GSI | |
|---|---|---|
| 削减载荷 | 是 | 是 |
| 削减读取成本 | **否 | 是** —— 按索引的大小读取 |
| 额外存储 | 无 | 被投影字段的第二份副本 |
| 额外写入成本 | 无 | 写入传播到索引 |
| 最适合 | 隐藏私有字段;小赢 | 大项里少数字段的热读取 |
这个取舍是诚实的:索引花你的存储和写入容量来省读取容量。对一个从重项里读薄薄一片的频繁读取,值得;
要削一次性的 GetItem,不值得。参见
GSI 与 LSI了解挑选索引类型,以及
一个 GSI 读取何时可能陈旧,在你把它放到热路径之前。
陷阱与下一步
- 别指望更小的账单。 单凭一个投影从不改变 RCU。如果数字没动,那是被记录的行为,而不是一个 bug。
- 给保留字加占位符。 表达式里一个裸的
name或status会使请求失败——用#给它映射。 - 投影键属性是免费的,而且常常有用——DynamoDB 便宜地返回它们,它们让你能分页或重新取出。
- 动用一个覆盖索引只在一个热模式从大项里读少数几个字段时;先权衡写入/存储成本。
在表达式构建器里构建 ProjectionExpression 及其属性名映射,
然后试试 DynoTable 对你自己的表运行这些投影,看着响应缩小。
[^rcu]:
AWS DynamoDB 开发者指南,处理读取与写入操作——读取容量基于应用任何 ProjectionExpression
之前的项大小。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/
- AWS DynamoDB 开发者指南,DynamoDB 中的保留字。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html ↩
- AWS DynamoDB 开发者指南,属性投影(
KEYS_ONLY/INCLUDE/ALL)。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html ↩