入門閱讀時間 2 分鐘

DynamoDB Projection Expression

一個 projection expression 是 DynamoDB 的 SELECT col1, col2:一份 以逗號分隔的屬性名稱清單,告訴 GetItemQueryScan 只回傳那些屬性,而非整個 item。

DynamoDB projection expression 能降低讀取成本嗎?

不能。ProjectionExpression 縮減的是回應 payload,而非你被計費的讀取容量。DynamoDB 從儲存讀取完整的 item,以 item 在磁碟上的大小計算 ,然後才在輸出途中丟棄你沒指名的屬性。若要真正降低讀取成本,請改用 covering

  • 它修剪 payload,而非讀取成本。 DynamoDB 從儲存讀取(並計費) 完整的 item,然後在輸出途中丟掉你沒指名的屬性。 ProjectionExpression 是一個網路最佳化,而非容量最佳化。
  • 它是你如何取得一個公開子集的方式。 指名一個呼叫者被 允許看到的少數屬性;其餘的絕不離開表。
  • 對任何可能是保留字的東西用 #name 佔位符。 expression 中 光禿禿的屬性名稱會與 DynamoDB 約 570 個保留字衝突 並讓請求失敗。
  • 要真正省讀取,改用 covering index。 一個只投影你需要的 欄位的 GSI,會以它自己(較小的)大小被讀取。

它實際省下什麼

從 SQL 過來,你會假設 SELECT a, b 掃描的比 SELECT * 少。在 DynamoDB 中那個直覺是錯的。一次讀取的 capacity unit 是從 item 在磁碟上的 大小 計算,向上取整到下一個 4 KB — 在投影被套用 之前。AWS 講得明確:一個 ProjectionExpression 不會改變一個請求消耗的 讀取容量。1

所以一個投影替你省兩件事,兩者都真實但都在讀取的下游:

  • 線上的位元組。 一個 6 KB item 以兩個小屬性回傳,是一個很小的 回應。在一個回傳數百個 item 的 Query 上,那加總得很快。
  • 客戶端工作。 較少東西要反序列化、較少要留在記憶體裡、較少會意外 洩漏進一個 log 或一個 API 回應。

省的是 RCU。那就是地雷:人們伸手去拿一個 投影來砍他們的帳單、看不到變化,於是斷定 DynamoDB 壞了。 它沒有 — 你量錯了那個槓桿。

投影一個公開使用者個人資料

假設你經營一個使用者目錄。每個個人資料是一個 item,以 你能依代號取得一個人的方式為 key:

PK = "PROFILE#ada"      (partition key)
SK = "PROFILE#ada"      (sort key — 單一 item 的 collection)

那個 item 很肥。它帶有帳戶的公開面,加上一堆 私密與操作性的屬性:

{
  "PK": "PROFILE#ada",
  "SK": "PROFILE#ada",
  "displayName": "Ada L.",
  "avatarUrl": "https://cdn.example.com/u/ada.png",
  "bio": "Builds things.",
  "emailAddress": "ada@example.com",
  "passwordResetToken": "…",
  "billingCustomerId": "cus_…",
  "lastLoginIp": "…",
  "internalRiskScore": 0.02
}

一張公開個人資料卡需要三個欄位。取得整個 item 意味著 emailAddresslastLoginIpinternalRiskScore 跑到一個 絕不該看到它們的脈絡。只指名公開子集:

GetItem  PK = "PROFILE#ada"  SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio

回應帶三個屬性。私密的那些留在表裡 — 不是被你的 app 在 抵達 之後 過濾掉,而是根本從未被序列化進回應。那就是 安全上的勝利,也是一旦一個機密已經越過一道邊界後就難以 撤銷的那一個。

你可以在 DynamoDB Expression Builder 中組裝並複製這個確切的請求 — 名稱、佔位符,以及 SDK 呼叫 — 它替你發出 ProjectionExpressionExpressionAttributeNames map。

# 佔位符逸出保留字

這裡是一個乾淨的投影爆炸之處。DynamoDB 保留了一長串 字 — namestatuscommentsizetimestamp,以及數百個其他的。2 如果你正在投影的某個屬性是其中之一,expression 中的原始 名稱會被拒絕。

假設那個個人資料也有一個 status 屬性("active""suspended")。 這會失敗:

ProjectionExpression   displayName, status

status 是保留的。修正是一個 expression attribute name — 一個 # 前綴的佔位符,對應到真實的名稱:

ProjectionExpression       displayName, #s
ExpressionAttributeNames   { "#s": "status" }

同樣的機制能伸進巢狀屬性。要從一個 map 拉出單一欄位、 或一個清單的一個元素,用 document-path 語法 — 並對每個 區段都用佔位符,因為它們之中任何一個都可能是保留的:

ProjectionExpression       #addr.#city, tags[0]
ExpressionAttributeNames   { "#addr": "address", "#city": "city" }

一條實用的規則:對每個東西都用佔位符。你永遠不必記住你正站在 約 570 個保留字裡的哪一個上,而 expression 兩種寫法讀起來都一樣。

何時 covering index 勝過投影

如果你真的需要砍讀取成本 — 而非只是 payload — 那個槓桿是一個 只投影你所讀屬性的 Global Secondary Index。一個 GSI 是 資料的一份獨立副本;你為它的投影選 KEYS_ONLYINCLUDEALL3 一個 KEYS_ONLY 或狹窄的 INCLUDE index 每個 item 實體上 較小,所以對它的一個 Query 以那個較小的大小被計量。

那是一個 covering index:查詢完全從 index 回答,不用 回 base table 一趟。當一個熱門讀取模式只需要大 item 中的少數 屬性時,用它。

ProjectionExpressionCovering GSI
砍 payload
砍讀取成本**否是** — 以 index 的大小讀取
額外儲存投影欄位的第二份副本
額外寫入成本寫入會傳播到 index
最適合隱藏私密欄位;小勝利從大 item 熱門讀取少數欄位

這個取捨是誠實的:index 花你儲存與寫入容量來省 讀取容量。對一個頻繁讀取重 item 中一薄片的情況值得; 對省掉一次性的 GetItem 不值得。挑 index 類型請見 GSI vs LSI,在你把一個 GSI 放上熱路徑之前,請見 何時一個 GSI 讀取可能陳舊

陷阱與下一步

  • 別期待一張較小的帳單。 單靠一個投影絕不改變 RCU。如果那個 數字沒動,那是有文件記載的行為,而非 bug。
  • 對保留字用佔位符。 expression 中一個光禿禿的 namestatus 會 讓請求失敗 — 用 #-map 它。
  • 投影 key 屬性是免費且常常有用的 — DynamoDB 便宜地回傳 它們,而它們讓你能分頁或重新取得。
  • 只在一個熱門模式從大 item 讀取少數欄位時,才伸手去拿 covering index;先權衡寫入/儲存成本。

Expression Builder 中建立 ProjectionExpression 與它的屬性名稱 map,並 試試 DynoTable 對你自己的表執行這些投影,並 看著回應縮小。


  1. AWS DynamoDB 開發人員指南,Working with Read and Write Operations — 讀取容量基於套用任何 ProjectionExpression 之前的 item 大小。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/
  2. AWS DynamoDB 開發人員指南,Reserved Words in DynamoDBhttps://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
  3. AWS DynamoDB 開發人員指南,Attribute ProjectionsKEYS_ONLY / INCLUDE / ALL)。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html

已更新