中階閱讀時間 3 分鐘

DynamoDB 篩選策略

DynamoDB 裡的“篩選”是四種不同的東西披著同一個詞。三種在資料被讀取並計費 之前 收窄它;一種——就是那個叫 Filter 的——在 之後 收窄它。分清哪個是哪個,就是這門功夫的大半。

DynamoDB 中的篩選是怎麼工作的?

DynamoDB 有四種篩選方式,只有一種是在你被計費之後才執行。 挑出一個分割區,排序索引鍵收窄一個切片,稀疏索引按屬性存在與否篩選——這三種都在計量之前削減你的讀取成本。一個 FilterExpression 在讀取之後執行,所以它縮小響應但絕不縮小賬單。

  • 是最便宜的篩選:它挑出分割區,所以你根本不碰表的其餘部分。
  • begins_withbetween<> 在一個分割區 篩選——仍在計費之前,仍然便宜。
  • 按缺失來篩選:一個項只有在擁有被索引的屬性時才出現在索引裡,所以索引 就是 那個篩選後的集合。
  • FilterExpression 是那個陷阱:它在 DynamoDB 計量讀取之後才執行,所以它削減你的響應大小,但絕不削減你的賬單。

搭好這個示例

一個產品目錄。一張表,分割區索引鍵 PK,排序索引鍵 SK

PK = "DEPT#kitchen"   SK = "PROD#00194"

每個產品還帶著 priceinStock(一個布林值)和 clearanceAt(一個 unix 時間戳,只在被標記為清倉的項上存在)。一個部門裡的項共享一個分割區,按產品 id 排序。

我們想要四種訪問模式。每一種對映到一種不同的篩選策略——而其中任何一種上的錯誤選擇,都是一次你要永遠付費的 Scan

按分割區索引鍵篩選

“給我 kitchen 裡的每一個產品。”分割區索引鍵直接回答這個:

Query  PK = "DEPT#kitchen"

DynamoDB 恰好讀取一個分割區。表裡其他任何東西都不被觸及或計費。這是在真正要緊的意義上唯一免費的篩選——它就是 QueryScan 之間的區別。

從 SQL 過來,這感覺是反的:沒有一個掃描索引的 WHERE department = 'kitchen',你只是 點名那個分割區。如果你點不出它的名,那是一個建模問題,不是一個查詢問題。

按排序索引鍵篩選

“給我從 PROD#00100 往上的 kitchen 產品。”排序索引鍵在分割區 收窄,而且它是在讀取被計量之前這麼做的:

Query  PK = "DEPT#kitchen"  AND  SK between "PROD#00100" AND "PROD#00200"

排序索引鍵條件被刻意限制:=<<=>>=betweenbegins_with。沒有 OR,沒有任意謂詞。

正是那個約束讓讀取保持有針對性——DynamoDB 走過一個連續的切片,而不是整個分割區。

這裡的槓桿是 你如何編碼排序索引鍵。如果你的模式是“按價格區間”,一個 PROD#<id> 排序索引鍵幫不上忙——你得把價格烤進鍵裡。

那是一個 排序索引鍵策略 決策,在設計時做出,不是在查詢時。

按稀疏索引篩選

“給我當前所有在清倉的東西。”大多數產品都不在清倉,所以你不想為了找出那少數幾個而讀取整個目錄。

一個 稀疏索引 透過缺失來解決這個問題。一個 只有在一個項擁有該索引的鍵屬性 兩者 時才包含這個項。

把一個常量 clearance = "CLEARANCE" 標誌設為 GSI 分割區索引鍵——只寫在清倉項上——以 clearanceAt 作為排序索引鍵,那麼索引就不含別的任何東西。

AWS 把這說明白了:一個全域次要索引只包含擁有該索引鍵屬性的項,所以缺少該鍵屬性的項根本不會被傳播過去(AWS——利用稀疏索引)。

基表 —— 所有產品 clearanceAt?複製到 ClearanceIndex不在索引中查詢該索引 = 只有清倉項

現在這個查詢唯讀取清倉項,也只為它們計費:

Query  ON ClearanceIndex   GSI_PK = "CLEARANCE"   (sorted by clearanceAt)

篩選發生在你 寫入 資料的時候——透過選擇到底要不要設 clearanceAt。索引就是那個篩選後的集合。關於哪種索引型別合適,參見 GSI 對比 LSI

用 FilterExpression 篩選

“給我有庫存的 kitchen 產品。”inStock 不是一個鍵屬性,所以你伸手去用一個 FilterExpression

Query  PK = "DEPT#kitchen"
Filter inStock = true

陷阱來了。DynamoDB 讀取 kitchen 分割區裡的每一個項,為它們全部計量容量,然後 才丟掉缺貨的那些。

官方規則:篩選運算式“在一次 Query 完成之後、但在結果被返回之前應用”,而且“無論是否存在篩選運算式,一次 Query 消耗相同數量的讀容量”——你已經為完整的讀取付了費(AWS——Query 的篩選運算式)。

所以如果 kitchen 有 10,000 個產品而 12 個有庫存,你要為讀取 10,000 個付費。響應很小;賬單不小。FilterExpression 縮小的是跨越網路傳輸的載荷,絕不是讀取。

還有第二道更鋒利的刃:分頁是在篩選 之前 計量的。一頁是 1 MB 被讀取的 項,而不是 1 MB 的匹配項。

一個篩選可以返回一個空頁卻帶著一個設好的 LastEvaluatedKey——DynamoDB 讀了整整一兆位元組,一個都沒匹配上,交給你一個空陣列。你繼續翻頁,而你為每一個空頁都付了費。

DynamoDB 運算式構建器 構建這個運算式——名稱、值,以及對保留字的正確轉義——讓 #inStock/:val 預留位置一次就對。

下面這個構建器預設為一次帶 FilterExpressionScan——正是上面那個反模式。注意這個篩選是在整張表上執行,而不是一個鍵切片:

建構你的請求
產生的程式碼
new ScanCommand({
  "TableName": "AuditLog",
  "FilterExpression": "#filter0 = :filterValue0",
  "ExpressionAttributeNames": {
    "#filter0": "action"
  },
  "ExpressionAttributeValues": {
    ":filterValue0": {
      "S": "delete"
    }
  }
})

對比這四種

何時篩選削減讀取成本?謂詞能力搭建成本
分割區索引鍵讀取之前是——一個分割區只有等值免費(它就是鍵)
排序索引鍵讀取之前是——一個切片範圍 / begins_with排序索引鍵設計
稀疏索引讀取之前是——僅索引一個屬性的存在額外的 GSI + 寫入成本
FilterExpression讀取之後幾乎任何條件

從上往下讀這張表:謂詞能力 上升,成本控制 下降FilterExpression 能精確地表達任何東西,正是因為它在已經讀取的項上執行——這也正是它無法替你省錢的原因。

在 DynoTable 中檢視

當你執行一個帶篩選的 Query 時,被 讀取 的項與被 返回 的項之間的差距就是整個故事。DynoTable 在被返回的項旁邊顯示被掃描的項——隨著一次篩選讀取的流入——這樣一個悄悄讀遍整個分割區的篩選就可見了,而不是藏在你的月度賬單裡。

對於真正的跨項問題——一個篩選回答不了的,比如“每個部門的平均價格”“有庫存的產品連同它們的評論”——DynoTable 的 SQL Workbench 在用戶端對一個有界的結果集執行 GROUP BYJOIN 和聚合,而不是編譯成一次表級的 Scan

陷阱與後續步驟

  • 別把 FilterExpression 當作你的主要訪問路徑。 如果一個模式很常見,就把它建模進一個鍵或一個稀疏索引。篩選是用來做最後那一點點收窄的,不是它的大頭。
  • 留意空頁。 一個帶篩選的查詢可以翻很久的頁卻什麼都不返回。尊重 LastEvaluatedKey;別假設一個空頁意味著“完了”。
  • 稀疏索引不是免費的。 它為每一個落入其中的項花費寫容量和儲存——當那個屬性稀有時便宜,當它不稀有時就沒那麼便宜。

定價計算器 估算一次帶篩選的讀取實際會花多少,並 試試 DynoTable,在你自己的表上盯著消耗的容量對比返回的行。

已更新