DynamoDB 项集合
项集合是一张表(或索引)中所有共享同一个值的项构成的集合。它不是你打开的某个功能——而是你键结构的一个自然产物。
只要两个项带有相同的分区键,它们就构成一个集合,而这个集合就成了 DynamoDB 允许你在单次 Query 中一起读取的单位。
这一点做对了,你的读取就一次往返返回。做错了,你就被困在 Scan 里。
什么是 DynamoDB 项集合?
DynamoDB 项集合是所有共享同一个值的项构成的集合,它们存储在一起并按排序键排序。它不是你启用的某个功能——它从你的键结构里自然涌现。集合是单次 Query 高效读取的单位,而 Scan 会遍历每一个分区。
- 集合无非就是“同一个分区键”。两个或更多带有相同分区键值的项被存储在一起,按排序。
- 它是高效
Query的单位。Query读取一个集合;Scan遍历每一个分区。这就是全部的性能故事。 - 没有排序键,就没有集合。一张只有分区键的表每个键只有一个项——没什么可集合的。
- 有两个限制会咬你:当存在 时每个集合 10 GB 的上限,以及低基数键带来的热分区。
问题所在:一起读取相关的项
假设你运营一支车队,每辆车每隔几秒钟就在流式上报遥测——车速、冷却液温度、油量。主导的读取是“给我车辆 V-7741 最近的读数”。
从 SQL 过来,你会给 vehicle_id 列建个索引,让规划器去干活。一个纯粹的键值存储可没这份奢侈。
它把每一条读数都当作一条孤立记录,所以那个问题就意味着扫描整张表再筛选。慢、贵,而且随着车队变大越来越糟。
DynamoDB 的答案是把“某辆车的所有读数”变成一个_物理上_成组、可直接寻址的东西。那个分组就是项集合。
集合到底是什么
DynamoDB 把项存储在分区里,它通过对分区键做哈希把每个项路由到某个分区。每个带有相同分区键值的项都被存储在一起,按排序键排序。它们从一个分区开始,但没有 LSI 时,DynamoDB 可以在排序键边界处把一个大或热的集合切分到多个分区上;只有 LSI 才会把整个集合钉在单个分区上(这也是为什么下面那个 10 GB 上限只针对 LSI)。
AWS 开发者指南准确地这样命名它。共享一个分区键值的那些项就是一个_项集合_,存储在一起并按排序键排序。
这和 2007 年那篇亚马逊 Dynamo 论文引入的思想是同一个——用一致性哈希把键分配到节点——再扩展出一个排序维度,让相关的项在磁盘上彼此相邻。
因为它们相邻且有序,DynamoDB 只需一次寻道就能返回它们中连续的一段。这就是为什么 Query 便宜而 Scan 不便宜:Query 读取单个集合;Scan 遍历每一个分区。
要构成一个集合,你需要一个 ——一个分区键_和_一个排序键。一张只按分区键建键的表,每个键值恰好只有一个项,所以没什么可集合的。
我们的实例:车辆 → 遥测读数
用一个复合键来给遥测流建模。分区键标识车辆;排序键是读数的时间戳,它让读数保持按时间戳排序(默认升序;传 ScanIndexForward=false 得到最新优先)。
| PK (vehicleId) | SK (recordedAt) | attributes |
|---|---|---|
| VEH#V-7741 | META | plate, model, depotCode |
| VEH#V-7741 | TS#2026-06-23T09:00:01Z | speedKph, coolantC, fuelPct |
| VEH#V-7741 | TS#2026-06-23T09:00:06Z | speedKph, coolantC, fuelPct |
| VEH#V-7741 | TS#2026-06-23T09:00:11Z | speedKph, coolantC, fuelPct |
| VEH#V-7742 | META | plate, model, depotCode |
| VEH#V-7742 | TS#2026-06-23T09:00:02Z | speedKph, coolantC, fuelPct |
这里住着两个集合——每辆车一个。META 项(车辆元数据)和 V-7741 的所有读数构成一个集合;V-7742 的各项构成另一个。
给元数据一个排在任何 TS#... 值_之前_的排序键(META),那么对 PK = "VEH#V-7741" 的单次 Query 就会把车辆的资料_和_它的读数一起返回。
这就是单表设计核心的那种父子模式。
每个虚线框都是一个项集合:同一个分区键,项按排序键排序。一次 Query 恰好读取一个框。
查询一个集合
因为集合按排序键排序,你免费得到区间读取。要拉取某辆车在十分钟窗口内记录的读数,你给排序键设定边界:
# Query
KeyConditionExpression vehicleId = :v AND recordedAt BETWEEN :from AND :to
ScanIndexForward false # newest first
键条件先把你限定到一个集合(vehicleId = :v),再限定到它连续的一片(recordedAt BETWEEN ...)。DynamoDB 只读取那些项,也只对它们计费。只想要元数据?recordedAt = "META" 取回那个唯一的 META 项。
手工构建这些键条件和投影表达式很琐碎。DynamoDB 表达式构建器 会替你生成 KeyConditionExpression、ExpressionAttributeNames 和 ExpressionAttributeValues,让保留字和占位符那些细节不咬你。
索引上的集合
一个二级索引有它_自己的_键结构,所以它构成它_自己的_项集合。
加一个按 depotCode(分区)和 recordedAt(排序)建键的全局二级索引,“来自车库 DEP-LON-3 的所有读数、最新优先”就变成对那个索引集合的一次 Query——一次基表无法提供的读取。
这就是为什么索引类型很重要:它决定了你能构成什么样的集合以及它们如何表现。权衡见 GSI 对比 LSI。
一个尖锐的区别:本地二级索引(LSI)共享基表的分区键,所以它的集合与基表项集合_在物理上绑在一起_——而这种绑定造成了一个硬限制,见下文。
会咬你的那些限制
项集合很强大,但有两个约束决定了你怎么塑造键:
- 10 GB 的 LSI 限制。当一张表有一个或多个本地二级索引时,单个项集合——某个分区键的基础项加上它们的 LSI 投影——不能超过 10 GB。超过它,让集合增长的写入就会以
ItemCollectionSizeLimitExceededException开始失败。一张_没有_ LSI 的表没有这种每集合上限。这正是为什么一个无界、不断增长的流(永不停歇的遥测)不适合 LSI:集合只会增长。GSI 有它自己的分区,所以它绕开了这个限制。 - 。一个集合住在一个分区里,而单个分区的吞吐量是有限的。如果某一辆车(或某一个
depotCode)吸走了极不成比例的一份流量,你就可能让那个分区热点化,哪怕整张表远在其预置吞吐量之下。自适应容量——在 AWS 的“Advanced Design Patterns for DynamoDB”re:Invent 深入分享里有讲——会自动隔离并加强热键,但它救不了一个完全没有分散的键。选高基数的分区键,让流量扇出到许多集合上。
在 DynoTable 中查看
为集合建立直觉最快的办法就是看一个。在 DynoTable 里,查询一个分区键会把整个集合渲染成一份连续的、按排序键排序的列表——META 项就坐在它那些带时间戳的读数前面,在屏幕上,无需任何脑内重建。

陷阱与后续步骤
- 没有排序键,就没有集合。一张只有分区键的表没法把相关的项分组。如果你需要一起读取项,你就需要一个复合键。
- 别让一个 LSI 集合无界增长。只追加的流属于 GSI(或一个按时间分桶的分区键),而不是 LSI,就因为那 10 GB 上限。
- 分散你的分区键。一个集合的可扩展性只等于它所在的那个分区。低基数的分区键会造成热点。
- 伸手去用
Query,而不是Scan。集合之所以存在,就是为了让你用一次有针对性的Query读取相关的项;退回到Scan就把这个优势扔掉了——参见 Query 对比 Scan。
画出你自己的键结构,对一个真实的分区键跑一次 Query,看着集合有序地返回。下载 DynoTable,直接探索你表里的集合。


