DynamoDB 篩選策略
DynamoDB 裡的“篩選”是四種不同的東西披著同一個詞。三種在資料被讀取並計費 之前 收窄它;一種——就是那個叫 Filter 的——在 之後 收窄它。分清哪個是哪個,就是這門功夫的大半。
DynamoDB 中的篩選是怎麼工作的?
DynamoDB 有四種篩選方式,只有一種是在你被計費之後才執行。 挑出一個分割區,排序索引鍵收窄一個切片,稀疏索引按屬性存在與否篩選——這三種都在計量之前削減你的讀取成本。一個 FilterExpression 在讀取之後執行,所以它縮小響應但絕不縮小賬單。
- 是最便宜的篩選:它挑出分割區,所以你根本不碰表的其餘部分。
- 用
begins_with、between、<、>在一個分割區 內 篩選——仍在計費之前,仍然便宜。 - 按缺失來篩選:一個項只有在擁有被索引的屬性時才出現在索引裡,所以索引 就是 那個篩選後的集合。
FilterExpression是那個陷阱:它在 DynamoDB 計量讀取之後才執行,所以它削減你的響應大小,但絕不削減你的賬單。
搭好這個示例
一個產品目錄。一張表,分割區索引鍵 PK,排序索引鍵 SK:
PK = "DEPT#kitchen" SK = "PROD#00194"
每個產品還帶著 price、inStock(一個布林值)和 clearanceAt(一個 unix 時間戳,只在被標記為清倉的項上存在)。一個部門裡的項共享一個分割區,按產品 id 排序。
我們想要四種訪問模式。每一種對映到一種不同的篩選策略——而其中任何一種上的錯誤選擇,都是一次你要永遠付費的 Scan。
按分割區索引鍵篩選
“給我 kitchen 裡的每一個產品。”分割區索引鍵直接回答這個:
Query PK = "DEPT#kitchen"
DynamoDB 恰好讀取一個分割區。表裡其他任何東西都不被觸及或計費。這是在真正要緊的意義上唯一免費的篩選——它就是 Query 和 Scan 之間的區別。
從 SQL 過來,這感覺是反的:沒有一個掃描索引的 WHERE department = 'kitchen',你只是 點名那個分割區。如果你點不出它的名,那是一個建模問題,不是一個查詢問題。
按排序索引鍵篩選
“給我從 PROD#00100 往上的 kitchen 產品。”排序索引鍵在分割區 內 收窄,而且它是在讀取被計量之前這麼做的:
Query PK = "DEPT#kitchen" AND SK between "PROD#00100" AND "PROD#00200"
排序索引鍵條件被刻意限制:=、<、<=、>、>=、between 和 begins_with。沒有 OR,沒有任意謂詞。
正是那個約束讓讀取保持有針對性——DynamoDB 走過一個連續的切片,而不是整個分割區。
這裡的槓桿是 你如何編碼排序索引鍵。如果你的模式是“按價格區間”,一個 PROD#<id> 排序索引鍵幫不上忙——你得把價格烤進鍵裡。
那是一個 排序索引鍵策略 決策,在設計時做出,不是在查詢時。
按稀疏索引篩選
“給我當前所有在清倉的東西。”大多數產品都不在清倉,所以你不想為了找出那少數幾個而讀取整個目錄。
一個 稀疏索引 透過缺失來解決這個問題。一個 只有在一個項擁有該索引的鍵屬性 兩者 時才包含這個項。
把一個常量 clearance = "CLEARANCE" 標誌設為 GSI 分割區索引鍵——只寫在清倉項上——以 clearanceAt 作為排序索引鍵,那麼索引就不含別的任何東西。
AWS 把這說明白了:一個全域次要索引只包含擁有該索引鍵屬性的項,所以缺少該鍵屬性的項根本不會被傳播過去(AWS——利用稀疏索引)。
現在這個查詢唯讀取清倉項,也只為它們計費:
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 預留位置一次就對。
下面這個構建器預設為一次帶 FilterExpression 的 Scan——正是上面那個反模式。注意這個篩選是在整張表上執行,而不是一個鍵切片:
對比這四種
| 何時篩選 | 削減讀取成本? | 謂詞能力 | 搭建成本 | |
|---|---|---|---|---|
| 分割區索引鍵 | 讀取之前 | 是——一個分割區 | 只有等值 | 免費(它就是鍵) |
| 排序索引鍵 | 讀取之前 | 是——一個切片 | 範圍 / begins_with | 排序索引鍵設計 |
| 稀疏索引 | 讀取之前 | 是——僅索引 | 一個屬性的存在 | 額外的 GSI + 寫入成本 |
| FilterExpression | 讀取之後 | 否 | 幾乎任何條件 | 無 |
從上往下讀這張表:謂詞能力 上升,成本控制 下降。FilterExpression 能精確地表達任何東西,正是因為它在已經讀取的項上執行——這也正是它無法替你省錢的原因。
在 DynoTable 中檢視
當你執行一個帶篩選的 Query 時,被 讀取 的項與被 返回 的項之間的差距就是整個故事。DynoTable 在被返回的項旁邊顯示被掃描的項——隨著一次篩選讀取的流入——這樣一個悄悄讀遍整個分割區的篩選就可見了,而不是藏在你的月度賬單裡。
對於真正的跨項問題——一個篩選回答不了的,比如“每個部門的平均價格”“有庫存的產品連同它們的評論”——DynoTable 的 SQL Workbench 在用戶端對一個有界的結果集執行 GROUP BY、JOIN 和聚合,而不是編譯成一次表級的 Scan。
陷阱與後續步驟
- 別把
FilterExpression當作你的主要訪問路徑。 如果一個模式很常見,就把它建模進一個鍵或一個稀疏索引。篩選是用來做最後那一點點收窄的,不是它的大頭。 - 留意空頁。 一個帶篩選的查詢可以翻很久的頁卻什麼都不返回。尊重
LastEvaluatedKey;別假設一個空頁意味著“完了”。 - 稀疏索引不是免費的。 它為每一個落入其中的項花費寫容量和儲存——當那個屬性稀有時便宜,當它不稀有時就沒那麼便宜。
用 定價計算器 估算一次帶篩選的讀取實際會花多少,並 試試 DynoTable,在你自己的表上盯著消耗的容量對比返回的行。