在會變化的屬性上給 DynamoDB 排序
你圍繞某個屬性建模排序索引鍵,好按它的順序查詢條目——然後這個屬性變了。一張工單的狀態、一個訂單的狀態、一個任務的優先順序。DynamoDB 拋給你的坑是:你不能就地更新鍵屬性。主索引鍵在條目的整個生命週期裡都是不可變的。改動一個身為鍵一部分的值,你就不是在編輯條目——你是在移動它,而 DynamoDB 逼你顯式地這麼做。
DynamoDB 的排序索引鍵能改嗎?
不能。排序索引鍵是主索引鍵的一部分,而 DynamoDB 的鍵屬性是不可變的——UpdateItem 無法編輯分割區索引鍵或排序索引鍵的值,也沒有"移動條目"操作。要改動它,就刪掉舊條目再放入一個新條目,或者改為把易變的值放在 GSI 排序索引鍵上。
- 鍵屬性是不可變的。你無法用
UpdateItem改動分割區索引鍵或排序索引鍵的值——DynamoDB 沒有"移動條目"操作。 - 要改動鍵值,就刪掉舊條目再放入一個新條目——最好放在一個事務裡,讓它保持原子性。
- 更好的做法:讓易變的值離開基表鍵,改把它放在 GSI 排序索引鍵上——GSI 鍵_可以_變化,因為更新基礎條目只會重新傳播索引項。
- 只要訪問模式允許,就選不會變的排序索引鍵(時間戳、不可變的 id)。
問題:你想拿來排序的狀態,卻一直在變
假設你營運一個技術支援臺,想按狀態給某個團隊的工單排序列出,於是你把狀態放進排序索引鍵:
PK: TEAM#7 SK: STATUS#open#TICKET#8842現在這張工單轉到了 pending。你想直接用 UpdateItem 把排序索引鍵改成 STATUS#pending#TICKET#8842——可 DynamoDB 拒絕任何改動鍵屬性的寫入。鍵是條目的地址;你沒法就地編輯這個地址。你選來排序的那個狀態,恰恰就是那個坐不住的東西。
方案一:刪除後重建(原子地)
如果這個值必須存在基表鍵裡,改動它就意味著移除舊條目、寫入新條目:
1. DeleteItem PK=TEAM#7 SK=STATUS#open#TICKET#8842
2. PutItem PK=TEAM#7 SK=STATUS#pending#TICKET#8842 (same attributes)把它放在一個 TransactWriteItems 裡,讓刪除和放入要麼都成功、要麼都失敗——否則兩者之間一旦崩潰,工單就會丟失或重複。這樣是可行的,但現在每一次狀態變化都是兩次寫入加上一個事務;對偶爾的變化沒問題,對頻繁的變化就代價高昂了。
方案二:讓可變值離開基表鍵(推薦)
更乾淨的設計:把基表鍵做成不可變的東西(工單 id),把那個易變、可排序的值放在 GSI 排序索引鍵上。
Base: PK: TICKET#8842 status: "open" teamId: TEAM#7
GSI: GSI1PK: TEAM#7 GSI1SK: STATUS#open#TICKET#8842現在改變狀態只是對基礎條目的 status 屬性做一次普通的 UpdateItem——這是 DynamoDB _允許_的,因為 status 不是基表鍵。DynamoDB 隨後會自動重新傳播 GSI 項到它排序後的新位置。一次 API 呼叫,原子性由它替你處理好——沒有事務,沒有刪除的那套折騰(底層 DynamoDB 仍然會刪掉舊索引項並寫入新的,所以一次帶索引的改動大約耗費 3 個寫入單位,而事務式的刪除加放入約為 4 個)。
取捨在於:GSI 是最終一致的,並且要額外的儲存/寫入——但對於一個經常變化的值,那比每次變化都刪除後重建要略便宜些(約 3 對 4 個寫入單位),也簡單得多。
在 DynoTable 中設計鍵
在 DynamoDB 運算式構建器裡為基表讀取和 GSI 讀取分別構造並預覽鍵條件。
在 DynoTable 裡,你接著挑選查詢要走哪個索引,看著易變的值在 GSI 上排序、而基礎條目保留它不可變的鍵——兩種讀取在真實資料上並排呈現。

陷阱與後續步驟
- 絕不要試圖用
UpdateItem改動鍵屬性——它會被拒絕;鍵值在條目的生命週期裡是固定的。 - 如果你必須移動它,就在事務裡做刪除加放入——絕不要當作兩次無防護的寫入。
- 對任何你既拿來排序_又_會改動的屬性,優先選不可變的基表鍵加一個 GSI。
- 別忘了 GSI 的最終一致——重新排序後的項要在短暫的傳播延遲之後才出現。
- 相關閱讀:排序索引鍵策略、GSI vs LSI、事務。
想看看一個可變屬性在 GSI 上和基表上分別是怎麼排序的?下載 DynoTable,直接探索你的索引。
寫入成本:刪除並放置 vs GSI 更新
us-east-1 點播中 1 KB 票證項目的粗略 WCU 比較(實際計費遵循AWS舍入規則):
| 圖案 | API 來電 | 典型的 WCU 影響 |
|---|---|---|
| 事務性刪除+放置基本金鑰 | TransactWriteItems(2 次操作) | 交易定價中每個操作的商品大小約為 2 倍 |
更新status屬性; GSI 重新傳播 | 一個 UpdateItem | 基本寫入 + GSI 寫入(1 KB 項目約 2 WCU + 預計屬性) |
GSI 這條路避開了應用層的編排,也消除了刪除與寫入之間一旦崩潰就會丟掉這一行的那個視窗。 你拿索引讀取上的最終一致性去換更簡單的寫入。 如果狀態每分鐘要變好幾次,就到定價計算器裡給你的項目大小和更新頻率建個模。
稀疏 GSI 用於狀態排序列表
如果只有 open 票證需要按狀態排序的佇列,請使用
sparse index:寫入GSI1PK = TEAM#7 並
GSI1SK = STATUS#open#...僅當status = open時。當售票截止時,更新時刪除或省略 GSI 關鍵屬性 — 該項目從基本排序索引鍵上沒有刪除並放置的索引。這樣可以保持索引較小,並避免對您從未列出的已關閉票證建立索引。
首選不可變的基本鍵
| 波動場 | 基臺 SK | 更好的基礎SK | 動盪的領域繼續存在 |
|---|---|---|---|
| 訂單狀態 | STATUS#shipped#ORD#99 | ORD#99 | GSI 排序或屬性 |
| 任務優先順序 | P#1#TASK#12 | TASK#12 | GSI 排序 |
| 使用者顯示名稱 | NAME#alice#USER#5 | USER#5 | 非關鍵屬性 |
時間戳和不可變 ID (CREATED#2026-06-27T10:00:00Z、TICKET#8842)
當您需要在基表上按時間順序排列時,建立穩定的基本排序索引鍵本身。
編碼前設計GSI
對映訪問模式
single-table design tool — 輸入“列表按團隊開票,優先順序”並檢查建議的 GSI1PK /
GSI1SK 模板。然後建立關鍵條件
expression builder 並行出分頁使用query builder查詢進行整合測試。
狀態更改後讀取您所寫的內容
在UpdateItem之後,對基表的強一致讀取顯示立即新status。對 GSI 的查詢可能會短暫滯後。 UI 流向重定向到 GSI 排序佇列應該容忍陳舊的行或從當精度很重要時,按 id 進行基表。


