不停機的 DynamoDB 遷移
從 SQL 過來,遷移是一個 ALTER TABLE,它在改寫每一列時鎖住表。DynamoDB 沒有綱要可以改——項目是無綱要的,所以加一個屬性或一個新的實體類型是免費的。
困難的部分是新資料必須服務的那個存取模式,以及在不做一次「停止整個世界」的改寫之下,把上線中的資料重塑成能服務它的形狀。
如何在不停機的情況下遷移 DynamoDB 表?
DynamoDB 沒有 ALTER TABLE,因此遷移永遠不會鎖住表。你可以用 UpdateTable 線上新增屬性、新的鍵形狀或 GSI,然後逐步重塑上線中的資料:在讀取時延遲回填舊項目,或進行節流清掃,並在過渡期間雙寫兩種格式。不存在一次性的「旗日(flag-day)」切換。
- 沒有
ALTER TABLE。 項目是無綱要的。一次「遷移」意味著加屬性、一個新的鍵形狀或一個新索引——絕不是改寫一組固定的欄位。 - 新寫入很簡單;舊項目才是問題。 既有的列不帶新屬性,所以任何新索引或查詢都會無聲地漏掉它們,直到你回填。
- 線上加索引,延遲回填。
UpdateTable在一張上線中的表上建一個 GSI;舊項目在讀取時回填(延遲),或用一次受控的清掃——絕不是一次「旗日(flag-day)」切換。 - 在過渡期間雙寫。 當兩種形狀並存時,把舊格式和新格式一起寫,這樣兩條讀取路徑都不會變舊。
把它框成一個存取模式,而不是一個欄
假設你在一張表上經營一個 SaaS 工作區產品。項目用 PK = "WS#<id>",而 SK 依實體超載:
| PK | SK | attributes |
|---|---|---|
| WS#a91 | META | name, tier |
| WS#a91 | DOC#2026-04-01#x7 | title, author, body |
| WS#a91 | DOC#2026-04-02#k2 | title, author, body |
現在產品想要文件上的留言,外加一個新的讀取:「列出某個成員在整個工作區裡寫的每一則留言,最新優先。」 最後那個子句就是那次遷移。光是一個新的實體類型不算什麼;服務一個目前的鍵回答不了的查詢才是工作。
先加那個新的實體類型
留言不過是同一個分割裡的新項目——沒有遷移儀式,沒有新表:
| PK | SK | attributes |
|---|---|---|
| WS#a91 | DOC#2026-04-01#x7#CMT#01HZ... | author, text, createdAt |
一個對 PK = "WS#a91" 帶 SK begins_with "DOC#2026-04-01#x7#CMT#" 的 Query,已經能列出一份文件的留言。既有的文件原封不動。這一半在第一天就上線——為什麼同一個分割能裝下兩者,見項目集合與超載鍵。
那個新查詢需要一個 GSI
「某個成員的所有留言,最新優先」沒辦法被基礎表服務——memberId 既不是 PK 也不是一個 SK 前綴。那是一個新索引,而正確地選它本身就是一個決定:見 GSI 與 LSI(一個 LSI 必須在建表時就存在,所以對一張上線中的表做遷移,GSI 是你唯一的選項)。
加一個通用的 GSI1,並把新屬性寫在新的留言項目上:
| GSI1PK | GSI1SK |
|---|---|
| MEMBER#u44 | 2026-04-02T09:15:00Z |
Query GSI1 WHERE GSI1PK = "MEMBER#u44" 帶 ScanIndexForward = false,給出每個成員最新優先的留言。
線上建索引
UpdateTable 對一張上線中的表加一個 GSI 而不停機。DynamoDB 在背景把既有項目回填進索引;索引在完成之前回報 CREATING/回填中,然後翻成 ACTIVE(管理 GSI)。
這裡有兩個陷阱。第一,AWS 警告,如果新鍵分佈不均,加一個 GSI 可能節流基礎表寫入——在低流量時段加它,並盯著 CloudWatch。第二,索引即使在它變成 ACTIVE 之後仍是最終一致;一次寫入可能有一瞬間不會在 GSI 上可見。見為什麼 GSI 是最終一致。
回填那些舊項目
GSI 只索引那些具有 GSI1PK/GSI1SK 的項目。你遷移前的留言——在那個屬性存在之前就寫入的——永遠不會出現,即使在回填完成之後。線上 GSI 回填會複製既有項目,但它沒辦法憑空造出不在那些項目上的屬性。你得把那些值加上去。
兩種策略:
| 策略 | 它如何運作 | 何時使用 |
|---|---|---|
| 延遲 | 在讀取一個舊項目時,把新屬性寫回去 | 舊項目常被讀取;把成本涓滴攤開 |
| 清掃 | 一次分頁的 Scan 把每一個舊項目更新一次 | 你需要在某個期限前讓 GSI 完整 |
對清掃,用 Scan 翻頁,並對每一則舊留言用一個條件式 UpdateItem 加上索引屬性,這樣你就永遠不會覆蓋一次並行的寫入。
那個條件守衛在「該屬性尚不存在」上。用 DynamoDB Expression Builder 建立並複製確切的 ConditionExpression 與 UpdateExpression,而不是手打 attribute_not_exists(GSI1PK)。
整個過渡期間雙寫
直到每一個舊項目都帶著新屬性,兩種形狀並存。寫入路徑必須在每一次寫入時都填入新格式——新留言以及對任何舊留言的更新——這樣那個落差才會只縮不增。
挑一個你能驗證的回填結束條件:清掃翻完了整張表,或延遲路徑跑得夠久、以致未轉換的項目照設計都已過時。只有到那時,你才移除舊的讀取路徑。略過這個,就是一次遷移在一小撮查詢無聲地回傳不完整結果時還「完成了」的方式。

陷阱
- 加上屬性 ≠ 回填完成。 一個新 GSI 對舊項目一開始是空的。在你信任那個查詢之前,先驗證覆蓋率。
- 就地改一個鍵不是遷移——是一次改寫。 你沒辦法變動一個項目的
PK/SK;你在新鍵之下寫一個新項目並刪掉舊的。把它規劃成「先複製後刪除」,中間雙讀。 - 沒有交易式切換。 沒有一個整張表翻轉的時刻。把每一步都設計成在兩種形狀都上線時安全。
下一步
在單表設計裡複查新鍵和超載的集合,並透過翻閱上線中的表來確認回填完成了。試用 DynoTable,去瀏覽你的表、找出未回填的項目,並對你自己的資料執行條件式更新。


