入門閱讀時間 3 分鐘

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-7741METAplate, model, depotCode
VEH#V-7741TS#2026-06-23T09:00:01ZspeedKph, coolantC, fuelPct
VEH#V-7741TS#2026-06-23T09:00:06ZspeedKph, coolantC, fuelPct
VEH#V-7741TS#2026-06-23T09:00:11ZspeedKph, coolantC, fuelPct
VEH#V-7742METAplate, model, depotCode
VEH#V-7742TS#2026-06-23T09:00:02ZspeedKph, coolantC, fuelPct

這裡住著兩個集合——每輛車一個。META 項(車輛後設資料)和 V-7741 的所有讀數構成一個集合;V-7742 的各項構成另一個。

注意這個巧妙之處:給後設資料一個排在任何 TS#... 值_之前_的排序索引鍵(META),那麼對 PK = "VEH#V-7741" 的單次 Query 就會把車輛的資料_和_它的讀數一起返回。

這就是單表設計核心的那種父子模式。

分区 · VEH#V-7742META —— 车辆资料TS#09:00:02分区 · VEH#V-7741META —— 车辆资料TS#09:00:01TS#09:00:06TS#09:00:11

每個虛線框都是一個項集合:同一個分割區索引鍵,項按排序索引鍵排序。一次 Query 恰好讀取一個框。

查詢一個集合

因為集合按排序索引鍵排序,你免費得到區間讀取。要拉取某輛車在十分鐘視窗內記錄的讀數,你給排序索引鍵設定邊界:

# Query
KeyConditionExpression   vehicleId = :v AND recordedAt BETWEEN :from AND :to
ScanIndexForward         false        # newest first

鍵條件先把你限定到一個集合(vehicleId = :v),再限定到它連續的一片(recordedAt BETWEEN ...)。DynamoDB 唯讀取那些項,也只對它們計費。只想要後設資料?recordedAt = "META" 取回那個唯一的 META 項。

手工構建這些鍵條件和投影運算式很瑣碎。DynamoDB 運算式構建器 會替你生成 KeyConditionExpressionExpressionAttributeNamesExpressionAttributeValues,讓保留字和預留位置那些細節不咬你。

索引上的集合

一個次要索引有它_自己的_鍵結構,所以它構成它_自己的_項集合。

加一個按 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 項就坐在它那些帶時間戳的讀數前面,在螢幕上,無需任何腦內重建。

在 DynoTable 中針對單個分割區索引鍵執行的 DynamoDB Query,按排序索引鍵順序顯示集合中的每個條目。
在 DynoTable 中針對單個分割區索引鍵執行的 DynamoDB Query,按排序索引鍵順序顯示集合中的每個條目。

陷阱與後續步驟

  • 沒有排序索引鍵,就沒有集合。一張只有分割區索引鍵的表沒法把相關的項分組。如果你需要一起讀取項,你就需要一個複合鍵。
  • 別讓一個 LSI 集合無界增長。只追加的流屬於 GSI(或一個按時間分桶的分割區索引鍵),而不是 LSI,就因為那 10 GB 上限。
  • 分散你的分割區索引鍵。一個集合的可擴充套件性只等於它所在的那個分割區。低基數的分割區索引鍵會造成熱點。
  • 伸手去用 Query,而不是 Scan集合之所以存在,就是為了讓你用一次有針對性的 Query 讀取相關的項;退回到 Scan 就把這個優勢扔掉了——參見 Query 對比 Scan

畫出你自己的鍵結構,對一個真實的分割區索引鍵跑一次 Query,看著集合有序地返回。下載 DynoTable,直接探索你表裡的集合。

已更新