DynamoDB 中的單一表格設計
從 SQL 過來,直覺是每個實體一張表:customers、orders、order_items。在 DynamoDB 中,那個直覺通常是錯的。一個儲存每一個實體、靠超載的鍵前綴加以區分的單一表格,讓你能在一個 Query 中擷取一個父項與它的所有子項 — 沒有 join,也沒有 N+1。
從存取模式開始,而非實體
單一表格設計是存取模式優先的。在你挑選單一的鍵之前,先寫下你的應用程式所做的每一次讀取 — 「取得某客戶的設定檔」、「列出某客戶由新到舊的訂單」、「找出所有未結訂單」 — 因為鍵的存在只是為了服務那份清單。關聯式正規化最佳化的是儲存;DynamoDB 建模最佳化的是你早已知道自己會跑的那些查詢。把它們一一列舉出來,然後設計鍵讓每一個都是單一的 Query。
構想
挑選通用的鍵名稱(PK、SK),並把實體類型編碼進值裡:
| PK | SK | attributes |
|---|---|---|
| CUSTOMER#42 | PROFILE | name, email, plan |
| CUSTOMER#42 | ORDER#2026-001 | total, status |
| CUSTOMER#42 | ORDER#2026-002 | total, status |
現在一個 Query PK = "CUSTOMER#42" 就會在單一計費的讀取中傳回設定檔以及每一筆訂單。SK begins_with "ORDER#" 則把它縮小到只有訂單。
視覺上,這些超載的項目以單一的項目集合堆疊在一個 partition key 之下:
一次對該 partition 的讀取,就把客戶與每一筆訂單一併交回。
超載的 GSI
同樣的技巧也適用於索引。在項目上放一個通用的 GSI1PK/GSI1SK,單一 GSI 就能依各項目寫入這些屬性的內容來服務多種存取模式:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| ORDER#001 | METADATA | STATUS#OPEN | 2026-01-04 |
| ORDER#002 | METADATA | STATUS#OPEN | 2026-01-05 |
現在 Query GSI1 WHERE GSI1PK = "STATUS#OPEN" 就會依日期列出未結訂單 — 一個基礎表格無法回答的模式。不同的實體可以用自己的意義重用 GSI1(例如 CATEGORY#books)。一個索引,多種查詢。
多對多:相鄰串列
對於關聯(一個使用者屬於多個團隊、一個團隊有多個使用者),把這條邊以對調的 id 寫兩次:PK=USER#1, SK=TEAM#9 與 PK=TEAM#9, SK=USER#1。查詢任一側就會列出另一側 — 這是 DynamoDB 對 join 表的替代做法。
何時不該用單一表格
它並非免費。一張超載的表格更難理解、更難演進,也對分析不友善。如果你的存取模式真的未知或不斷變化,或資料大多是分析性的,那麼分開的表格(或不同的儲存)可能是更明智的選擇。當模式是已知且高流量時,單一表格才會勝出。
錯誤形狀的成本
把它建模成分開的表格,會迫使你用 Scan 或用戶端 join 來重組一個客戶,而那正是 Scan 陷阱。先建模存取模式,然後設計鍵讓每一個都成為一個 Query。
用項目大小與容量計算機估算這些項目每次讀取的成本,並試用 DynoTable,瀏覽一個單一表格 schema,並排看到那些超載的集合。