中階閱讀時間 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,把它們跑在你自己的表上,看清每個鍵條件到底返回哪一片。

已更新