中級読了 2 分

DynamoDB の Condition Expression 完全ガイド(例付き)

条件式は、DynamoDB が書き込みをコミットする に既存のアイテムに対して評価する 述語です。述語が偽なら書き込みは拒否され、何も変わりません。これは DynamoDB が持つ、 書き込みに対する WHERE 句にもっとも近いもの — そして不変条件を強制する唯一の安全な 方法です。

DynamoDB の条件式はどう機能するのか?

条件式は、書き込みをコミットする前に、DynamoDB が現在のアイテムに対してサーバー側で評価する述語です。真なら書き込みは進み、偽なら書き込みは ConditionalCheckFailedException で拒否され、何も変わりません。チェックと変更を 1 つのアトミックな操作にまとめるため、並行する呼び出し元が古い読み取りで競合することはありません。

  • これはフィルタではなくガード。 ConditionExpression は現在のアイテムに対して サーバー側で走り、結果が偽なら書き込みは ConditionalCheckFailedException で失敗します。
  • read-then-write を置き換える。 SELECT してから UPDATE する往復はなく、チェックと 変更が 1 つのアトミックな操作なので、2 つの呼び出し元が競合できません。
  • 拒否は無料でも、実行は無料ではない。 失敗した条件付き書き込みでも書き込みキャパシティを 消費します。拒否された書き込みは、照合対象となった既存アイテムのサイズ分の WCU(最小 1)を 課金します — 失敗した create-if-absent は 1 WCU かかります。

SQL から来ると、行を読み、アプリコードでチェックし、それから更新するでしょう。DynamoDB では、その読み取りと書き込みの間のギャップが、並行呼び出し元を待ち受けるデータ破損バグに なります。条件式はそのギャップを閉じます。

どこに適用されるか

ConditionExpressionPutItemUpdateItemDeleteItem、そして TransactWriteItems の各アクションに付けられます。QueryScan の一部では ありません — それらは読み取りパスの別物である FilterExpression を使います。

この区別は人を混乱させるので、正確に言いましょう。

ConditionExpressionFilterExpression
パス書き込み(Put/Update/Delete読み取り(Query/Scan
失敗時の効果書き込み全体を拒否結果からアイテムを除外
見るもの書き込み前の現在のアイテム各候補アイテム(読み取り後)
コスト失敗した書き込みも課金されるフィルタされたアイテムも読み取り分は課金される

どちらもサーバー側で走ります。違いは「偽」が何をするかです — 条件は変更を中止し、フィルタは すでに読み取り分を支払った行をただ隠すだけです。 (AWS: Condition Expressions

実際に使う関数

条件言語は小さいものです。主力はこれ。

  • attribute_exists(path) / attribute_not_exists(path) — この は アイテムに存在するか?「なければ作成のみ」「あれば更新のみ」の定番のイディオムです。
  • 比較演算子 — =<><<=>>= — 値または別の属性に対して。
  • attribute_typebegins_withcontainssize — 型と文字列/セットのチェック。
  • BETWEEN … AND …IN (…) — 範囲とメンバーシップ。
  • ANDORNOT、括弧 — 上記を組み合わせるため。

に対する attribute_not_exists は、PutItem を既存アイテムを 上書きしないインサートのように振る舞わせる定石です — DynamoDB には独立した「insert」操作が ないため、条件 インサートのセマンティクスそのものになります。 (AWS: Comparison Operator and Function Reference

実践例:残高不足から台帳を守る

銀行の台帳を例に取ります。各口座は 1 つのアイテムです。

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

不変条件はこうです。引き落としは利用可能残高を決してゼロ未満に押し下げてはならず、 存在しない口座を引き落としてはなりません。2 つのルールで、どちらも書き込み自体の中で 強制できます。

誤ったやり方(地雷)

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

GetItemUpdateItem の間で、2 つ目の引き落としが同じ 50000 を読み、自分の チェックを通過し、同じく書き込むことができます。両方が成功し、口座はマイナスになります。 これは read-modify-write の競合であり、アプリ側でいくら検証しても直りません — チェックと 書き込みが別々の操作だからです。

正しいやり方

チェックを書き込みに折り込みます。口座が存在し かつ 十分な残高を持つことを条件に、 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 は一時的なエラーではありません — 同じ書き込みを リトライしても何も変わりません。ビジネスルールが発火したことを意味します。残高不足、 重複作成、古いバージョンです。インフラの一過性の障害ではなく、ドメインの結果として 表面化させましょう。

失敗をデバッグ可能にするものが 2 つあります。

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB が失敗と一緒に現在の アイテムを返すので、2 回目の読み取りなしに「残高は 20000 でしたが、30000 を要求しました」 と示せます。 (AWS: Working with Items
  • 2 つの失敗理由を区別する。 attribute_exists(PK) AND clearedCents >= :amt は 「口座なし」と「残高不足」を 1 つの例外にまとめてしまいます。呼び出し元がそれらを 区別する必要があるなら、2 つの書き込みに分割するか、返されたアイテムを調べましょう。

楽観的ロックも同じトリック

バージョン番号パターンは、別の帽子をかぶった条件式にすぎません。version 属性を保存し、 どの書き込みも読み取ったバージョンを表明し、それをインクリメントします。

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

別の書き手が先に動いていれば、version = :seen は偽となり、書き込みは拒否され、再読み込み してリトライします。これが DynamoDB がロックなしで並行性制御を行う方法です — 見たものを 表明し、それが動いていたら失敗する。(AWS: Optimistic Locking with Version Number) DynoTable のステージングエリアはこのパターンを代わりに実行します — 並行編集は、失われた書き込みではなく、解決すべき競合として表面化します。

落とし穴と次のステップ

  • 予約語と衝突する名前。 statussizename、その他 ~570 語は予約されています。 ExpressionAttributeNames#s = status)でエイリアスするか、リクエストは ValidationException('Attribute name is a reserved keyword')で拒否されます。 予約語チェッカーは、属性名を受け取り、 そのまま貼り付けられるエイリアスマップを返します。
  • 条件は別のアイテムを参照できない。 書き込まれるアイテムしか見えません。アイテムを またぐ不変条件には、アクションごとの ConditionExpression を伴う TransactWriteItems、 またはセンチネルアイテムに対する ConditionCheck が必要です。
  • 失敗した書き込みでも WCU はかかる。 90% の確率で拒否するガードでも、その拒否分を 課金します。安い保険ですが、無料ではありません。

これらのガードが走るキーのモデル化については、シングルテーブル設計Query と Scan を参照してください。実データに対して条件付き 書き込みを発行する準備ができたら、DynoTable をダウンロード して自分の テーブルに対して実行しましょう。

更新日