进阶阅读约 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,对着你自己的表运行它们。

更新于