DynamoDB 键条件表达式
一个键条件表达式是你传给 Query 的 KeyConditionExpression——请求中 DynamoDB 唯一用来查找项
的部分。其他一切(过滤、投影)都在读取已被计量之后才运行。
DynamoDB 中的键条件表达式是什么?
键条件表达式是传给 Query 的 KeyConditionExpression,用于告诉 DynamoDB 读取哪些项。必须是相等条件(PK = :v);接受一个范围运算符——=、<、<=、>、>=、BETWEEN 或 begins_with。它决定了什么被读取并计费,而不同于过滤。
- 分区键必须是相等。
PK = :v,别无其他——没有范围、没有begins_with、没有IN。DynamoDB 对它做哈希以定位一个分区。 - 排序键接受一个范围运算符。
=、<、<=、>、>=、BETWEEN或begins_with——这里是你 切一个项集合的地方。 - 它不是过滤。 一个键条件决定什么被读取并计费;一个
FilterExpression只在你为读取付费之后 修剪结果。 - 排序键是字节有序的。 范围运算符按字典序比较,所以你如何格式化排序键字符串就是你的查询能力。
为什么分区键被锁定为相等
DynamoDB 通过对分区键做哈希到一个物理分区来存储项。一个哈希给你一个位置,而不是一个范围——所以没什么 可跨越去扫描的。
这就是为什么 PK > :v 或 begins_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: falseScanIndexForward: 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_with 和 BETWEEN 的语法弄对。
在 DynoTable 中看它
对一个真实的频道分区运行同一个键条件,看着已消耗的容量实时更新,于是你可以确认你读的是一个切片 ——而不是整个集合。
那个坑:把键条件和过滤搞混
那个昂贵的错误是动用 FilterExpression 去做键的活儿。
KeyConditionExpression: ChannelRef = :ch
FilterExpression: begins_with(PostedAt, :day)这看起来等价于上面那个 begins_with 键条件,并返回相同的行——但它先读取整个频道分区,然后丢弃
当天之外的一切。你为完整读取被计费。
过滤从不减少读取成本。它们在 DynamoDB 计量过项之后才运行,这与一个过滤的 Scan
是同一个坑。如果一个谓词能放进键条件,它就该在那里。
修法在上游:如果一个访问模式无法表达为一个 PK 相等加一个排序键范围,那是一个建模信号。要么重塑排序键, 要么加一个为该模式设键的索引——参见 GSI 与 LSI 和 单表设计了解如何布置键。
陷阱与下一步
- 分区键永远是
=。 绝无范围。如果你需要跨分区的一个范围,你已经超出了单次Query的能力。 - 每个查询一个排序键条件。 你无法
AND两个排序键谓词;选BETWEEN或begins_with,不能两者皆有。 - 保留字需要别名。 一个名为
Timestamp或Name的键必须用ExpressionAttributeNames(#ts), 否则查询报错。(AWS: 保留字) BETWEEN是包含的。 两个端点都被匹配——据此设计你的边界。
在表达式构建器里起草你的键条件,然后 试试 DynoTable 对你自己的表运行它们,看着它们实际消耗的容量。