中階閱讀時間 2 分鐘

DynamoDB 中的單一表格設計

從 SQL 過來,直覺是每個實體一張表:customersordersorder_items。在 DynamoDB 中,那個直覺通常是錯的。一個儲存每一個實體、靠超載的鍵前綴加以區分的單一表格,讓你能在一個 Query 中擷取一個父項與它的所有子項 — 沒有 join,也沒有 N+1。

從存取模式開始,而非實體

單一表格設計是存取模式優先的。在你挑選單一的鍵之前,先寫下你的應用程式所做的每一次讀取 — 「取得某客戶的設定檔」、「列出某客戶由新到舊的訂單」、「找出所有未結訂單」 — 因為鍵的存在只是為了服務那份清單。關聯式正規化最佳化的是儲存;DynamoDB 建模最佳化的是你早已知道自己會跑的那些查詢。把它們一一列舉出來,然後設計鍵讓每一個都是單一的 Query

構想

挑選通用的鍵名稱(PKSK),並把實體類型編碼進值裡:

PKSKattributes
CUSTOMER#42PROFILEname, email, plan
CUSTOMER#42ORDER#2026-001total, status
CUSTOMER#42ORDER#2026-002total, status

現在一個 Query PK = "CUSTOMER#42" 就會在單一計費的讀取中傳回設定檔以及每一筆訂單。SK begins_with "ORDER#" 則把它縮小到只有訂單。

視覺上,這些超載的項目以單一的項目集合堆疊在一個 partition key 之下:

Partition: CUSTOMER#42SK: PROFILESK: ORDER#2026-001SK: ORDER#2026-002One Query

一次對該 partition 的讀取,就把客戶與每一筆訂單一併交回。

超載的 GSI

同樣的技巧也適用於索引。在項目上放一個通用的 GSI1PK/GSI1SK,單一 GSI 就能依各項目寫入這些屬性的內容來服務多種存取模式:

PKSKGSI1PKGSI1SK
ORDER#001METADATASTATUS#OPEN2026-01-04
ORDER#002METADATASTATUS#OPEN2026-01-05

現在 Query GSI1 WHERE GSI1PK = "STATUS#OPEN" 就會依日期列出未結訂單 — 一個基礎表格無法回答的模式。不同的實體可以用自己的意義重用 GSI1(例如 CATEGORY#books)。一個索引,多種查詢。

多對多:相鄰串列

對於關聯(一個使用者屬於多個團隊、一個團隊有多個使用者),把這條邊以對調的 id 寫兩次PK=USER#1, SK=TEAM#9PK=TEAM#9, SK=USER#1。查詢任一側就會列出另一側 — 這是 DynamoDB 對 join 表的替代做法。

何時不該用單一表格

它並非免費。一張超載的表格更難理解、更難演進,也對分析不友善。如果你的存取模式真的未知或不斷變化,或資料大多是分析性的,那麼分開的表格(或不同的儲存)可能是更明智的選擇。當模式是已知且高流量時,單一表格才會勝出。

錯誤形狀的成本

把它建模成分開的表格,會迫使你用 Scan 或用戶端 join 來重組一個客戶,而那正是 Scan 陷阱。先建模存取模式,然後設計鍵讓每一個都成為一個 Query

項目大小與容量計算機估算這些項目每次讀取的成本,並試用 DynoTable,瀏覽一個單一表格 schema,並排看到那些超載的集合。

已更新