DynamoDB 裡什麼時候不該用單表設計
單表設計是 DynamoDB 的預設建議,而它配得上:一次 Query 把一個父項目和它的子項目交回來,沒有 join,沒有 N+1。
但它是一個取捨——你用一個僵硬、不透明的綱要去買讀取速度。有些工作負載付不起那個代價,而硬把一張表套上它,本身就是一個地雷。
什麼時候不該在 DynamoDB 裡用單表設計?
當你的工作負載是繁重的 OLAP 分析、少數不相關實體的普通 CRUD,或是需要獨立擴展與獨立失敗的實體時,請避免使用單表設計。在這些情況下,多張表更易讀、成本相同,且保有彈性。只有當存取模式是已知的、相關的且高量時,單表設計才真正勝出。
- 繁重的分析? 別用單表。超載的鍵對 OLAP 充滿敵意——匯出到一個欄式儲存,在那裡查詢。
- 只有少數幾種存取模式的普通 CRUD? 每個實體一張表很好、好讀,而且在效能上不花你什麼。
- 獨立擴展或失敗的實體? 分開的表讓你能各自地調校、計費、控制爆炸半徑。
- 單表仍然勝出,當你的模式是已知、相關且高量時——那正是它為之而生的情況。
認清單表實際上的成本
單表設計不是免費的;它只是把成本從讀取路徑移到其他一切上頭。你用易讀性和彈性付帳。
一張在 PK/SK 後面裝五種實體類型的表,難讀、難讓人上手、也難改。一個新存取模式可能意味著一次橫跨那個分割裡每一種項目類型的回填。
所以問題不是「單表好不好?」而是「我的存取模式撐得起這份僵硬嗎?」當它們撐不起時,伸手去抓多張表。
別把分析工作負載做成單表
DynamoDB 是為 OLTP 打造的——小的、已知的、點與範圍的讀取。分析是 OLAP:GROUP BY、大型聚合、橫跨整個資料集的臨機切片。這兩者往相反方向拉。
單表設計讓 OLAP 更糟,而不是更好。超載的鍵和混雜的實體類型,意味著一個分析作業在能加總任何東西之前,必須先解開哪個項目是哪個——這跟一次乾淨的欄式掃描恰恰相反。
從 SQL 過來,那個反射是把聚合寫去打那張上線中的表。在 DynamoDB 裡那是一次完整的 Scan——你付費並讀取每一個項目,那就是全量的 Scan 地雷。
解法不是一個更聰明的鍵。而是一個不同的儲存。AWS 自己的指引是把 DynamoDB 匯出到 S3,並用一個像 Athena 的查詢引擎跑分析。
把 OLTP 和 OLAP 放在不同的引擎上(AWS DynamoDB Developer Guide,S3DataExport.html)。
別把簡單的 CRUD 做成單表
如果你的應用依各自的 id 讀寫幾個不相關的實體,而你大概有三種存取模式,單表給你的好處是零。
拿一個小型 B2B 工具的一張 Tenants 表和一張 ApiKeys 表:
| TenantId | Name | PlanTier |
|---|---|---|
| TNT-8842 | Northwind | pro |
| KeyId | TenantId | Scope |
|---|---|---|
| KEY-01H9... | TNT-8842 | read-write |
每一個查詢都是依 id 的一次 GetItem,或對一個以 TenantId 設鍵的 GSI 的一次 Query。沒有父與子的擷取要最佳化,所以一個超載的分割沒什麼好贏的。兩張清楚的表比一張混濁的讀起來更好。
陷阱是因為單表是「最佳實務」就盲從它。最佳實務是存取模式優先。如果那些模式是瑣碎的,簡單的形狀就是對的形狀。
把獨立擴展或失敗的表拆開
一張單一的表共用一個輸送量面、一個備份、一個爆炸半徑。當兩種實體有截然不同的寫入速率或耐久度需求時,那個共享的命運就變成一項負債。
想像一個車隊追蹤系統。車輛很少改變;它們的遙測每秒灌進來:
| VehicleId | Make | Model | Region |
|---|---|---|---|
| VEH-204 | Volvo | FH16 | eu-west |
| DeviceTs | VehicleId | SpeedKph | Fuel |
|---|---|---|---|
| 2026-06-23T10:00:01Z | VEH-204 | 88 | 0.61 |
兩張表讓你能為一場消防水管般的洪流佈建遙測、把車輛保持得小而便宜、只對遙測設一個 TTL,並阻止一場遙測寫入風暴節流到車輛目錄的讀取。一張表把這一切全綁在一起。
依 2007 年的 Amazon Dynamo 論文,分割和可用性是頭等的關注點——獨立的表給你對兩者的獨立控制。
在你提交之前把決策畫出來
讓工作負載走過一道閘門:那些實體相關嗎,而那些模式已知且高量嗎?如果不是,多張表。
以下是這個判斷的流程——從頂端開始,跟著第一個符合的分支走:
只有右下角那片葉子配得上單表;每一條其他路徑都被多於一張的表服務得更好。
單表 vs 多張表,正面對決
| 因素 | 單表 | 多張表 |
|---|---|---|
| 相關讀取 | 一次 Query,沒有 join | 用戶端 join 或額外往返 |
| 易讀性 | 不透明的超載鍵 | 一個實體一張表,自帶說明 |
| 新存取模式 | 往往是一次回填 | 隔離地加一張表或 GSI |
| 分析 / OLAP | 充滿敵意——加總前要先解開 | 仍要匯出,但每個實體更乾淨 |
| 獨立擴展 | 共用的輸送量 + 爆炸半徑 | 分開調校、計費、TTL 與備份 |
| 最適合時 | 已知、相關、高量的模式 | 不相關、演進中或分析性的 |
陷阱 + 下一步
那個鏡像的錯誤是矯枉過正——把真正相關、高量的實體拆成一個實體一張表,並在你的應用裡重建 SQL 風格的 join。那正是單表所殺掉的 N+1 陷阱。
挑那個存取模式所要的形狀,不是教條。
當你確實要建模關係時,倚賴正確的索引類型——在你加一個之前先看 GSI 與 LSI。
當你確實要對一個多表綱要寫一個查詢時,先在 DynamoDB Expression Builder 裡草擬 KeyConditionExpression。
那樣你就能在一個全 Scan 的形狀打進生產之前先逮到它。
然後試用 DynoTable,對你自己的表瀏覽兩種形狀,看看你的存取模式實際上要哪一種。