DynamoDB 投影運算式
投影運算式是 DynamoDB 裡的 SELECT col1, col2:一個逗號分隔的名列表,告訴 GetItem、Query 或 Scan 只返回那些屬性,而不是整個條目。
DynamoDB 投影運算式能降低讀取成本嗎?
不能。ProjectionExpression 裁剪的是響應載荷,而不是你被計費的讀取容量。DynamoDB 從儲存裡讀取完整條目,按它在磁碟上的大小計量 ,然後在返回途中丟掉你沒點名的屬性。要真正削減讀取成本,改用一個覆蓋式 。
- 它裁剪載荷,而不是讀取成本。DynamoDB 從儲存裡讀取(並計費)完整條目,然後在返回途中丟掉你沒點名的屬性。
ProjectionExpression是一種網路最佳化,而不是容量最佳化。 - 它是你抓取一個公開子集的方式。點名呼叫方被允許看到的那幾個屬性;其餘的永遠不離開表。
- 對任何可能是保留字的東西用
#name預留位置。運算式裡的裸屬性名會與 DynamoDB 約 570 個保留字衝突,導致請求失敗。 - 要真正省讀取,改用一個覆蓋式索引。一個只投影你所需列的 ,會以它自己(更小的)大小被讀取。
它實際省下什麼
從 SQL 過來,你會以為 SELECT a, b 比 SELECT * 掃描得更少。在 DynamoDB 裡這個直覺是錯的。一次讀取的容量單位是由條目在磁碟上的大小算出來的,向上取整到下一個 4 KB——而且是在應用投影_之前_。AWS 說得很明白:ProjectionExpression 不改變一個請求消耗的讀取容量。1
所以投影為你省下兩樣東西,兩樣都是真的,但兩樣都是讀取的下游:
- 線路上的位元組。一個 6 KB 的條目作為兩個小屬性返回,是個很小的響應。在一個返回上百個條目的
Query上,這加起來很快就可觀了。 - 用戶端的工作。更少的反序列化,更少的記憶體佔用,更少意外洩漏進日誌或 API 響應的東西。
它不省的是 RCU。這就是那個陷阱:人們伸手去用投影來削減賬單,看到沒變化,就斷定 DynamoDB 壞了。它沒壞——你量錯了槓桿。
投影一個公開的使用者資料
假設你營運一個使用者目錄。每份資料是一個條目,鍵設計得能按使用者名稱抓一個人:
PK = "PROFILE#ada" (partition key)
SK = "PROFILE#ada" (sort key — single-item collection)
這個條目很胖。它既攜帶帳戶的公開面孔,又攜帶一堆私有和營運屬性:
{
"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
}一張公開的資料卡片需要三個欄位。抓取整個條目意味著 emailAddress、lastLoginIp 和 internalRiskScore 會傳到一個永遠不該看到它們的上下文裡。只點名那個公開子集:
GetItem PK = "PROFILE#ada" SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio
響應攜帶三個屬性。私有的那些留在表裡——不是被你的應用在到達_之後_過濾掉,而是壓根從未被序列化進響應。這就是安全上的勝利,而且它是那種一旦秘密已經越過邊界就很難挽回的勝利。
你可以在 DynamoDB 運算式構建器裡組裝並複製這個確切的請求——名稱、預留位置,以及 SDK 呼叫——它會替你產出 ProjectionExpression 和 ExpressionAttributeNames 對映。
在下面的預設裡增刪欄位,看著 ProjectionExpression 變化——只有列出的屬性會返回:
用 # 預留位置轉義保留字
這裡就是一個乾淨的投影會炸掉的地方。DynamoDB 保留了一長串詞——name、status、comment、size、timestamp,還有上百個。2如果你正在投影的某個屬性是其中之一,運算式裡的裸名就會被拒絕。
假設這份資料還有一個 status 屬性("active"、"suspended")。這會失敗:
ProjectionExpression displayName, status
status 是保留字。修復方法是一個運算式屬性名——一個 # 字首的預留位置,對映到真實名稱:
ProjectionExpression displayName, #s
ExpressionAttributeNames { "#s": "status" }
同樣的機制能伸進巢狀屬性。要從一個 map 裡取出單個欄位,或從一個 list 裡取一個元素,用文件路徑語法——並且給每一段都加預留位置,因為其中任何一段都可能是保留字:
ProjectionExpression #addr.#city, tags[0]
ExpressionAttributeNames { "#addr": "address", "#city": "city" }
一條實用規則:給一切都加預留位置。你就永遠不必記住你正踩在那約 570 個保留字裡的哪一個上,而且運算式兩種寫法讀起來都一樣。而如果你更想知道究竟是哪些名稱出了問題,把它們貼上進保留字檢查器——它會標記衝突,並輸出 ExpressionAttributeNames 別名對映。
覆蓋式索引何時勝過投影
如果你真心需要削減讀取成本——不只是載荷——那個槓桿是一個只投影你所讀屬性的全域次要索引。GSI 是資料的一份單獨副本;你為它的投影選擇 KEYS_ONLY、INCLUDE 或 ALL。3一個 KEYS_ONLY 或窄 INCLUDE 的索引每條目在物理上更小,所以針對它的 Query 會以那個更小的大小被計量。
這就是覆蓋式索引:查詢完全從索引裡得到回答,不用回基表跑一趟。當一個熱讀取模式永遠只需要大條目裡的幾個屬性時用它。
ProjectionExpression | 覆蓋式 GSI | |
|---|---|---|
| 裁剪載荷 | 是 | 是 |
| 削減讀取成本 | 否 | 是——以索引的大小讀取 |
| 額外儲存 | 無 | 被投影欄位的第二份副本 |
| 額外寫入成本 | 無 | 寫入會傳播到索引 |
| 最適合 | 隱藏私有欄位;小贏 | 從大條目裡熱讀取幾個欄位 |
取捨是誠實的:索引花你儲存和寫入容量來省下讀取容量。對於從一個重條目裡頻繁讀取一個薄切片,值得;為了省下一次性的 GetItem,不值得。參見 GSI vs LSI 來挑選索引型別,並在你把索引放到熱路徑上之前看看GSI 讀取何時可能是陳舊的。
陷阱與後續步驟
- 別指望賬單變小。光靠投影永遠不改變 RCU。如果數字沒動,那是有據可查的行為,不是 bug。
- 給保留字加預留位置。運算式裡一個裸的
name或status會讓請求失敗——用#對映它。 - 總是包含鍵屬性——它們幾乎不增載入荷,還讓你能分頁或重新抓取條目。
- 只在一個熱模式從大條目裡讀取幾個欄位時才伸手去用覆蓋式索引;先權衡寫入/儲存成本。
在運算式構建器裡構造 ProjectionExpression 及其屬性名對映,然後試試 DynoTable,對你自己的表執行這些投影,看著響應縮小。
- AWS DynamoDB 開發者指南,《Using projection expressions in DynamoDB》——讀取容量基於應用任何
ProjectionExpression之前的條目大小。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html ↩ - AWS DynamoDB 開發者指南,《Reserved Words in DynamoDB》。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html ↩
- AWS DynamoDB 開發者指南,《Attribute Projections》(
KEYS_ONLY/INCLUDE/ALL)。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html ↩