进阶阅读约 2 分钟

DynamoDB 键条件表达式

一个键条件表达式是你传给 QueryKeyConditionExpression——请求中 DynamoDB 唯一用来查找项 的部分。其他一切(过滤、投影)都在读取已被计量之后才运行。

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

键条件表达式是传给 QueryKeyConditionExpression,用于告诉 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 里,那里你已经付了费。 (AWS: 键条件表达式

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

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

  • 分区键 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 的语法弄对。

在 DynoTable 中看它

对一个真实的频道分区运行同一个键条件,看着已消耗的容量实时更新,于是你可以确认你读的是一个切片 ——而不是整个集合。

那个坑:把键条件和过滤搞混

那个昂贵的错误是动用 FilterExpression 去做键的活儿。

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

看起来等价于上面那个 begins_with 键条件,并返回相同的行——但它先读取整个频道分区,然后丢弃 当天之外的一切。你为完整读取被计费。

过滤从不减少读取成本。它们在 DynamoDB 计量过项之后才运行,这与一个过滤的 Scan 是同一个坑。如果一个谓词能放进键条件,它就该在那里。

修法在上游:如果一个访问模式无法表达为一个 PK 相等加一个排序键范围,那是一个建模信号。要么重塑排序键, 要么加一个为该模式设键的索引——参见 GSI 与 LSI单表设计了解如何布置键。

陷阱与下一步

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

表达式构建器里起草你的键条件,然后 试试 DynoTable 对你自己的表运行它们,看着它们实际消耗的容量。

更新于