进阶阅读约 2 分钟

DynamoDB 键条件表达式

键条件表达式就是你传给 QueryKeyConditionExpression——请求里唯一被 DynamoDB 用来_查找_项的部分。其余的一切(筛选、投影)都在读取被计量之后才运行。

什么是 DynamoDB 中的键条件表达式?

键条件表达式是 Query 上的 KeyConditionExpression,它告诉 DynamoDB 要读取哪些项。必须是一个等值(PK = :v);接受一个区间运算符——=<<=>>=BETWEENbegins_with。它决定什么被读取和计费,这一点和筛选不同。

  • 必须是一个等值。PK = :v,别无其他——没有区间,没有 begins_with,没有 IN。DynamoDB 对它做哈希以定位单个分区。
  • 接受一个区间运算符。=<<=>>=BETWEENbegins_with——这里是你切一个的地方。
  • 它不是一个筛选。键条件决定什么被_读取_和计费;一个 FilterExpression 只在你为读取付过费之后裁剪结果。
  • 排序键是按字节排序的。区间运算符按字典序比较,所以你怎么格式化排序键字符串,_就是_你的查询能力。

为什么分区键被锁定为等值

DynamoDB 通过对分区键做哈希来把项存到一个物理分区上。哈希给你一个位置,而不是一个区间——所以没有什么可供_跨越_扫描的。

这就是为什么 PK > :vbegins_with(PK, :v) 会被直接拒绝。引擎没法在不读取整张表的情况下回答“所有键以 X 开头的分区”,而那恰恰是它被造出来要避免的那个 Scan

从 SQL 过来,这感觉是反的:WHERE id LIKE 'order%' 在 Postgres 里再普通不过。而在 DynamoDB 里,分区键是一个地址,不是一个可搜索的列。

能力所在之处是排序键

在一个分区内,项是按排序键排序存储的。正是那个排序被区间运算符利用——DynamoDB 寻道到一个位置然后往前读。

运算符读取用于
SK = :v一个精确的项按键取一个特定的子项
SK < / <= / > / >= :v一个开放端的切片“这个点之后的一切”
SK BETWEEN :a AND :b一个闭区间(含端点)一个有界窗口——一个日期范围
begins_with(SK, :p)一个前缀切片PK 下的某种类型或层级

键上没有 LIKE、没有 CONTAINS、没有 ENDS_WITH。子串和后缀匹配不是按字节排序的,所以它们会逼出一次全量读取——这是设计使然,API 不会让你这么做。子串匹配可以通过 FilterExpression 里的 contains() 实现(那时你已经为读取付过费了);后缀匹配在服务端根本不可用——存一个反转的键,或者在客户端筛选它。(AWS:键条件表达式

一个实例:聊天应用里的消息

假设你在构建基于频道的聊天。一张表,按频道分区,按消息时间排序。原始键结构:

  • 分区键 ChannelRef —— CH#{channelId}
  • 排序键 PostedAt —— 一个 ISO-8601 时间戳,MSG#2026-06-23T14:05:00Z

MSG# 前缀让消息行保持可排序,并与你可能共同放在同一频道下的任何其他行类型(置顶配置、成员关系)区分开。

加载一个频道最新的消息。只用分区键,最新优先:

KeyConditionExpression      ChannelRef = :ch
ExpressionAttributeValues   { ":ch": "CH#general" }
ScanIndexForward            false

ScanIndexForward: false 反向遍历已排序的集合——不用在客户端排序就能拿到“最近优先”的便宜办法。

begins_with 取某个特定的一天。因为时间戳是排序键、且以文本存储,一个日期前缀就是一个干净的切片:

KeyConditionExpression  ChannelRef = :ch AND begins_with(PostedAt, :day)
:ch    "CH#general"
:day   "MSG#2026-06-23"

那读取 2026-06-23 这一天的每一条消息、别无其他——DynamoDB 寻道到前缀,一旦走出末尾就停下。这之所以行得通,只因为前缀是一个按字节排序的字符串的真正左锚。

BETWEEN 取一个精确的窗口。对于“14:00 那一小时里的消息”,一个含端点的区间胜过一个前缀:

KeyConditionExpression  ChannelRef = :ch AND PostedAt BETWEEN :lo AND :hi
:ch    "CH#general"
:lo    "MSG#2026-06-23T14:00:00Z"
:hi    "MSG#2026-06-23T14:59:59Z"

BETWEEN 对两端都是含端点的,所以慎重挑你的端点——这里差一个都会悄悄漏掉或重复一条边界消息。

你可以在 DynamoDB 表达式构建器 里组装并复制上面任何一个表达式,ExpressionAttributeValues 映射也替你填好了——一次就把 begins_withBETWEEN 的语法弄对,很顺手。

这个构建器预设为一个 pk = … AND begins_with(sk, …) 查询——改一下运算符,看 KeyConditionExpression 随之更新:

构建你的请求
生成的代码
new QueryCommand({
  "TableName": "AuditLog",
  "KeyConditionExpression": "#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)",
  "ExpressionAttributeNames": {
    "#hashKey": "pk",
    "#rangeKey": "sk"
  },
  "ExpressionAttributeValues": {
    ":hashKeyValue": {
      "S": "TENANT#acme"
    },
    ":rangeKeyValue": {
      "S": "EVENT#2026-06"
    }
  }
})

在 DynoTable 中查看

对一个真实的频道分区跑同样的键条件。你一设定分区键筛选,DynoTable 就发出一次 Query——于是你只加载那一片,而不是整个集合。

陷阱:把键条件和筛选搞混

那个昂贵的错误是伸手去用 FilterExpression 干键该干的活。一个筛选甚至没法引用 PostedAt——它是排序键,而 DynamoDB 会以 ValidationException 拒绝对一个键属性的筛选。所以变通办法是把日期复制进一个普通的、非键的属性(MessageDate),转而对它筛选:

KeyConditionExpression   ChannelRef = :ch
FilterExpression         begins_with(MessageDate, :day)

这_看起来_和上面那个 begins_with 键条件等价,也返回同样的行——但它先读取整个频道分区,然后丢弃那一天之外的一切。你要为整次读取付费。

筛选从不削减读取成本。它们在 DynamoDB 计量过那些项之后才运行,和一个带筛选的 Scan 是同一个暗坑。如果一个谓词能进键条件,它就该待在那里。

在上游修复它。如果一个访问模式没法表达成一个 PK 等值加一个排序键区间,那就是一个建模信号。要么重塑排序键,要么加一个为该模式建键的索引——参见 GSI 对比 LSI单表设计,了解如何布局这些键。

陷阱与后续步骤

  • 分区键永远是 =永远没有区间。如果你需要一个跨分区的区间,你就已经长出了单次 Query 的范围。
  • 每次查询一个排序键条件。你没法 AND 两个排序键谓词;挑 BETWEEN begins_with,不能都要。
  • 保留字需要别名。一个名叫 TimestampName 的键必须用 ExpressionAttributeNames#ts),否则查询报错。(AWS:保留字
  • BETWEEN 含端点。两个端点都被匹配——据此设计你的边界。

表达式构建器里起草你的键条件,然后试试 DynoTable,把它们跑在你自己的表上,看清每个键条件到底返回哪一片。

更新于