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 里的 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_with 和 BETWEEN 的语法弄对,很顺手。
这个构建器预设为一个 pk = … AND begins_with(sk, …) 查询——改一下运算符,看 KeyConditionExpression 随之更新:
在 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,不能都要。 - 保留字需要别名。一个名叫
Timestamp或Name的键必须用ExpressionAttributeNames(#ts),否则查询报错。(AWS:保留字) BETWEEN含端点。两个端点都被匹配——据此设计你的边界。
在表达式构建器里起草你的键条件,然后试试 DynoTable,把它们跑在你自己的表上,看清每个键条件到底返回哪一片。