中階閱讀時間 2 分鐘

DynamoDB 條件運算式完整指南(含示例)

條件運算式是 DynamoDB 在提交你的寫入 之前 對現有項求值的一個謂詞。如果謂詞為假,寫入被拒絕,什麼也不會改變。它是 DynamoDB 裡最接近於寫入上的 WHERE 子句的東西——也是強制執行一個不變式的唯一安全方式。

DynamoDB 條件運算式是怎麼工作的?

條件運算式是 DynamoDB 在提交一次寫入之前,在伺服器端針對當前項求值的一個謂詞。如果為真,寫入繼續;如果為假,寫入被 ConditionalCheckFailedException 拒絕,什麼也不會改變。它把檢查和變更摺疊進一個原子操作,所以並行的呼叫方無法基於一次陳舊讀取來搶跑。

  • 它是一道守衛,不是一個篩選。 ConditionExpression 在伺服器端對當前項執行;結果為假就用 ConditionalCheckFailedException 讓寫入失敗。
  • 它取代了讀-然後-寫。 沒有先 SELECTUPDATE 的往返——檢查和變更是一個原子操作,所以兩個呼叫方無法互相競態。
  • 拒絕是免費的,執行不是。 一次失敗的條件寫入仍然消耗寫容量。一次被拒絕的寫入會按它所檢查的現有項的大小計費 WCU(最少 1)——一次失敗的“不存在才建立”花費 1 WCU。

從 SQL 過來,你會讀出那一行,在應用程式碼裡檢查它,然後更新。在 DynamoDB 裡,讀和寫之間的那個間隙是一個正等著某個並行呼叫方來觸發的資料損壞 bug。條件運算式關掉了這個間隙。

它們在哪裡適用

你把一個 ConditionExpression 附加到 PutItemUpdateItemDeleteItem,以及 TransactWriteItems 裡的每一個動作上。它 不是 QueryScan 的一部分——那些用的是 FilterExpression,那是讀取路徑上的另一回事。

這個區別容易把人絆倒,所以說精確點:

ConditionExpressionFilterExpression
路徑寫入(Put/Update/Delete讀取(Query/Scan
失敗時的效果拒絕整次寫入把項從結果中丟棄
看到的是當前項,寫入之前每個候選項,讀取之後
成本失敗的寫入照樣計費被篩掉的項仍按讀取計費

兩者都在伺服器端執行。區別在於“為假”會做什麼:條件會中止一次變更;篩選只是隱藏一行你已經付費讀取的資料。 (AWS:條件運算式

你實際會用到的函式

條件語言很小。主力選手:

  • attribute_exists(path) / attribute_not_exists(path)——這個 在項上存不存在?這是“只在不存在時建立”/“只在存在時更新”的經典慣用法。
  • 比較符——=<><<=>>=——對著一個值或另一個屬性。
  • attribute_typebegins_withcontainssize——型別與字串/集合檢查。
  • BETWEEN … AND …IN (…)——範圍與成員判定。
  • ANDORNOT、括號——用來組合以上這些。

上用 attribute_not_exists 是讓 PutItem 表現得像一次不會覆蓋現有項的插入的規範方式——DynamoDB 沒有單獨的“insert”操作,所以這個條件 就是 插入語義。 (AWS:比較運算子與函式參考

一個實戰示例:守護一個賬本不被透支

拿一個銀行賬本來說。每個帳戶是一個項:

PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents    = 0

不變式:一次借記絕不能把可用餘額推到零以下,而且你絕不能借記一個不存在的帳戶。兩條規則,都能在寫入本身裡強制執行。

錯誤的做法(那個暗雷)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← app-side check
UpdateItem  SET clearedCents = 20000

GetItemUpdateItem 之間,第二次借記可以讀到同一個 50000,透過它自己的檢查,然後也寫進去。兩者都成功了;帳戶變成了負數。這是一個讀-改-寫競態,任何數量的應用端校驗都修不了它——檢查和寫入是分開的操作。

正確的做法

把檢查摺疊進寫入。借記 30000 分,條件是帳戶存在 餘額足夠:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = clearedCents - :amt
  ConditionExpression:
    attribute_exists(PK) AND clearedCents >= :amt

其中 :amt = 30000。如果餘額過低,或者該項從未被建立過,DynamoDB 就用 ConditionalCheckFailedException 拒絕寫入,餘額絲毫不動。並行的那次借記,要麼看到原始餘額並對著它被檢查,要麼看到更新後的餘額——絕不會基於一次它據以行動的陳舊讀取。

你可以用 DynamoDB 運算式構建器 構建並複製那個精確的運算式——名稱、值,一應俱全——而不用手工拼裝 ExpressionAttributeValues 對映。

就在這裡試試——這個構建器預設為一次帶守衛的 PutItemattribute_not_exists),這樣你就能讀到生成的 ConditionExpression

建構你的請求
產生的程式碼
new PutItemCommand({
  "TableName": "AuditLog",
  "Item": {
    "pk": {
      "S": "TENANT#acme"
    },
    "sk": {
      "S": "EVENT#2026-06-24T10:00:00Z"
    },
    "action": {
      "S": "login"
    }
  },
  "ConditionExpression": "attribute_not_exists(#cond0)",
  "ExpressionAttributeNames": {
    "#cond0": "pk"
  }
})

在 DynoTable 中檢視這道守衛

當一次條件寫入失敗時,你想看到項的真實狀態,而不是去猜。把帳戶項調出來,直接讀 clearedCents

DynoTable 中的賬本集合——BALANCE 項在帳戶的交易項上方顯示 clearedCents。
DynoTable 中的賬本集合——BALANCE 項在帳戶的交易項上方顯示 clearedCents。

讀懂這次拒絕,別盲目重試

ConditionalCheckFailedException 不是一個瞬時錯誤——重試同一次寫入什麼也改變不了。它意味著一條業務規則觸發了:資金不足、重複建立、版本陳舊。把它當作一個領域結果來呈現,而不是一次基礎設施抖動。

有兩樣東西能讓失敗可除錯:

  • ReturnValuesOnConditionCheckFailure: ALL_OLD——DynamoDB 在返回失敗的同時返回當前項,所以你不用第二次讀取就能展示“餘額是 20000,你要了 30000”。 (AWS:使用項
  • 區分兩種失敗原因。 attribute_exists(PK) AND clearedCents >= :amt 把“沒有帳戶”和“沒有資金”坍縮成了一個異常。如果呼叫方需要把它們分辨開,就拆成兩次寫入,或者檢視返回的項。

樂觀鎖是同一個訣竅

版本號模式只是換了頂帽子的條件運算式。存一個 version 屬性;每次寫入都斷言你讀到的那個版本並把它加一:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = :new, version = :next
  ConditionExpression: version = :seen

如果另一個寫入方先動了手,version = :seen 就為假,寫入被拒絕,你就重新讀取並重試。這就是 DynamoDB 在無鎖的情況下做並行控制的方式——斷言你所看到的,如果它變了就失敗。(AWS:使用版本號的樂觀鎖)DynoTable 的暫存區會替你執行這個模式——並行編輯會作為一個待解決的衝突浮現出來,而不是一次丟失的寫入。

陷阱與後續步驟

  • 與保留字衝突的名稱。 statussizename 以及大約 570 個其他詞是保留字。用 ExpressionAttributeNames 給它們起別名(#s = status),否則請求會被一個 ValidationException 拒絕(“Attribute name is a reserved keyword”)。保留字檢查器接收你的屬性名,並交回一份可直接貼上的別名對映。
  • 一個條件無法引用另一個項。 它只看得到正被寫入的那個項。跨項的不變式需要帶每動作 ConditionExpressionTransactWriteItems,或者對著一個哨兵項做一次 ConditionCheck
  • 失敗的寫入照樣花 WCU。 一道 90% 的時間都在拒絕的守衛,仍然要為那些拒絕計費。便宜的保險,但不是免費的。

關於為這些守衛所對著的鍵建模,參見 單表設計Query 對比 Scan。當你準備好對著真實資料發起條件寫入時,下載 DynoTable,對著你自己的表執行它們。

已更新