進階閱讀時間 3 分鐘

不停機的 DynamoDB 遷移

從 SQL 過來,遷移是一個 ALTER TABLE,它在改寫每一列時鎖住表。DynamoDB 沒有綱要可以改——項目是無綱要的,所以加一個屬性或一個新的實體類型是免費的。

困難的部分是新資料必須服務的那個存取模式,以及在不做一次「停止整個世界」的改寫之下,把上線中的資料重塑成能服務它的形狀。

如何在不停機的情況下遷移 DynamoDB 表?

DynamoDB 沒有 ALTER TABLE,因此遷移永遠不會鎖住表。你可以用 UpdateTable 線上新增屬性、新的鍵形狀或 GSI,然後逐步重塑上線中的資料:在讀取時延遲回填舊項目,或進行節流清掃,並在過渡期間雙寫兩種格式。不存在一次性的「旗日(flag-day)」切換。

  • 沒有 ALTER TABLE 項目是無綱要的。一次「遷移」意味著加屬性、一個新的鍵形狀或一個新索引——絕不是改寫一組固定的欄位。
  • 新寫入很簡單;舊項目才是問題。 既有的列不帶新屬性,所以任何新索引或查詢都會無聲地漏掉它們,直到你回填。
  • 線上加索引,延遲回填。 UpdateTable 在一張上線中的表上建一個 GSI;舊項目在讀取時回填(延遲),或用一次受控的清掃——絕不是一次「旗日(flag-day)」切換。
  • 在過渡期間雙寫。 當兩種形狀並存時,把舊格式和新格式一起寫,這樣兩條讀取路徑都不會變舊。

把它框成一個存取模式,而不是一個欄

假設你在一張表上經營一個 SaaS 工作區產品。項目用 PK = "WS#<id>",而 SK 依實體超載:

PKSKattributes
WS#a91METAname, tier
WS#a91DOC#2026-04-01#x7title, author, body
WS#a91DOC#2026-04-02#k2title, author, body

現在產品想要文件上的留言,外加一個新的讀取:「列出某個成員在整個工作區裡寫的每一則留言,最新優先。」 最後那個子句就是那次遷移。光是一個新的實體類型不算什麼;服務一個目前的鍵回答不了的查詢才是工作。

先加那個新的實體類型

留言不過是同一個分割裡的新項目——沒有遷移儀式,沒有新表:

PKSKattributes
WS#a91DOC#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,並把新屬性寫在新的留言項目上:

GSI1PKGSI1SK
MEMBER#u442026-04-02T09:15:00Z

Query GSI1 WHERE GSI1PK = "MEMBER#u44"ScanIndexForward = false,給出每個成員最新優先的留言。

線上建索引

UpdateTable 對一張上線中的表加一個 GSI 而不停機。DynamoDB 在背景把既有項目回填進索引;索引在完成之前回報 CREATING/回填中,然後翻成 ACTIVE管理 GSI)。

UpdateTable:加 GSI1索引狀態:CREATING回填既有項目狀態:ACTIVE查詢 GSI1 安全

這裡有兩個陷阱。第一,AWS 警告,如果新鍵分佈不均,加一個 GSI 可能節流基礎表寫入——在低流量時段加它,並盯著 CloudWatch。第二,索引即使在它變成 ACTIVE 之後仍是最終一致;一次寫入可能有一瞬間不會在 GSI 上可見。見為什麼 GSI 是最終一致

回填那些舊項目

GSI 只索引那些具有 GSI1PKGSI1SK 的項目。你遷移前的留言——在那個屬性存在之前就寫入的——永遠不會出現,即使在回填完成之後。線上 GSI 回填會複製既有項目,但它沒辦法憑空造出不在那些項目上的屬性。你得把那些值加上去。

兩種策略:

策略它如何運作何時使用
延遲在讀取一個舊項目時,把新屬性寫回去舊項目常被讀取;把成本涓滴攤開
清掃一次分頁的 Scan 把每一個舊項目更新一次你需要在某個期限前讓 GSI 完整

對清掃,用 Scan 翻頁,並對每一則舊留言用一個條件式 UpdateItem 加上索引屬性,這樣你就永遠不會覆蓋一次並行的寫入。

那個條件守衛在「該屬性尚不存在」上。用 DynamoDB Expression Builder 建立並複製確切的 ConditionExpressionUpdateExpression,而不是手打 attribute_not_exists(GSI1PK)

整個過渡期間雙寫

直到每一個舊項目都帶著新屬性,兩種形狀並存。寫入路徑必須在每一次寫入時都填入新格式——新留言以及對任何舊留言的更新——這樣那個落差才會只縮不增。

挑一個你能驗證的回填結束條件:清掃翻完了整張表,或延遲路徑跑得夠久、以致未轉換的項目照設計都已過時。只有到那時,你才移除舊的讀取路徑。略過這個,就是一次遷移在一小撮查詢無聲地回傳不完整結果時還「完成了」的方式。

在回填期間,於 DynoTable 裡翻一張表的頁,去找出缺了新索引屬性的項目。
在回填期間,於 DynoTable 裡翻一張表的頁,去找出缺了新索引屬性的項目。

陷阱

  • 加上屬性 ≠ 回填完成。 一個新 GSI 對舊項目一開始是空的。在你信任那個查詢之前,先驗證覆蓋率。
  • 就地改一個鍵不是遷移——是一次改寫。 你沒辦法變動一個項目的 PKSK;你在新鍵之下寫一個新項目並刪掉舊的。把它規劃成「先複製後刪除」,中間雙讀。
  • 沒有交易式切換。 沒有一個整張表翻轉的時刻。把每一步都設計成在兩種形狀都上線時安全。

下一步

單表設計裡複查新鍵和超載的集合,並透過翻閱上線中的表來確認回填完成了。試用 DynoTable,去瀏覽你的表、找出未回填的項目,並對你自己的資料執行條件式更新。

已更新