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,直接探索你表裡的集合。


