DynamoDB 中的 Type 屬性
在 SQL 裡,一筆資料列所屬的資料表「就是」它的類型——documents
裡的一列就是一份文件。DynamoDB 的單一資料表把所有實體混在同一個 schema
底下,所以一個項目本身並沒有內建答案來回答「這是什麼?」。
Type 屬性 把那個答案放回去:每個項目上一個單純的字串, 標明它所代表的實體。
DynamoDB 中的 Type 屬性是什麼?
Type 屬性是一個你烙在每個項目上的純字串——例如 EntityType: "Document"——用來標明該項目所代表的實體。由於單一資料表把許多實體混在同一個 schema 底下,項目本身並沒有內建的類型。Type 屬性把它補回去,讓你的程式碼能辨識資料列、把 GSI 篩選成單一實體,並在遷移時存活下來。
- 每次寫入都烙上一個 Type。 每個項目一個屬性——
EntityType: "Document"—— 毫無例外。它只花幾個位元組,卻能在日後救你一命。 - 它在混合分區中辨識實體。 一次
Query會一起回傳工作區、 文件和留言;Type 會告訴你的程式碼哪個是哪個, 不必去解析鍵的前綴。 - 它讓 GSI 上的單一實體篩選成為可能。 把 Type 投影進索引, 你就能把一個多載索引收窄到剛好一種實體類型。
- 它是你做遷移時的逃生口。 當你匯出來重新建模、或把某個實體 搬到自己的資料表時,Type 就是你用來切分的那一欄。
為什麼混合資料表會弄丟類型
單一資料表設計把所有實體都存進一張
資料表,背後用 PK、SK 這種通用鍵。這正是重點所在——一次
Query 就能一起回傳一個父項與它的子項。但這也意味著一個分區是
異質的。
拿一個 SaaS 文件協作應用來說。一個工作區分區裡裝著工作區記錄、 它的文件,以及那些文件上的留言:
| PK | SK | attributes |
|---|---|---|
| WS#acme | META | name, plan, seats |
| WS#acme | DOC#a1#META | title, owner, wordCount |
| WS#acme | DOC#a1#CMT#0007 | author, body, createdAt |
| WS#acme | DOC#a1#CMT#0008 | author, body, createdAt |
Query PK = "WS#acme" 會在一次計費的讀取中把這四個項目都交回給你。現在你的
程式碼拿到一串原始項目,卻沒有可靠的方法說出哪個是文件、
哪個是留言——除非去字串比對 SK,而那種做法在你的鍵格式一改變的
瞬間就會變得脆弱。
在每個項目上烙上 Type
修法就是每次寫入都加一個屬性,標明實體:
| PK | SK | EntityType | title |
|---|---|---|---|
| WS#acme | META | Workspace | — |
| WS#acme | DOC#a1#META | Document | Q3 Roadmap |
| WS#acme | DOC#a1#CMT#0007 | Comment | — |
在 item.EntityType === "Document" 上分支,是一個穩定的相等檢查。
解析 SK.startsWith("DOC#") && SK.includes("#CMT#") 則是一種猜測,會在你
改版鍵的時候出問題。Type 讓你的讀取邏輯與鍵的編碼解耦——
那才是真正的勝利。
一次讀取回傳三種實體類型;Type 屬性把每個項目路由到 正確的處理器,完全不必碰鍵。
把 GSI 收窄到單一實體
Type 在索引上才真正派上用場。假設你加了一個 GSI,鍵是
GSI1PK = WS#acme、GSI1SK = updatedAt,用來列出「這個工作區裡最近改過的
所有東西,最新的排前面」。一個多載索引會把文件「和」留言一起掃進來——
但一個資訊流 UI 可能只想要文件。
有兩種收窄方式,差別在於錢:
| 做法 | 它的成本 | 何時使用 |
|---|---|---|
在 Type 上用 FilterExpression | 讀取所有符合鍵的項目、為全部計費,讀取後才丟掉不符的 | 結果中混入的實體很少;想快速上線 |
稀疏索引(Type 放進 GSI1PK) | 只有你要的實體才會進入索引 | 某一種實體占多數;你想零浪費 |
FilterExpression 是在項目被讀取之後、且在容量被
消耗之後才執行——AWS 明確指出篩選並不會降低讀取成本
(DynamoDB 開發人員指南:FilterExpression)。
在 Type 上篩選是誠實的,不是免費的:你為那些被丟掉的留言付了錢。
要把資訊流收窄成只有文件,查詢會帶一個對 Type
屬性的條件。用 DynamoDB 運算式建構器
來組裝 FilterExpression、名稱與值——它會產生
#t = :doc 佔位符,讓你不會手滑打到保留字。
KeyConditionExpression GSI1PK = :ws
FilterExpression #t = :doc
ExpressionAttributeNames { "#t": "EntityType" }
ExpressionAttributeValues { ":ws": "WS#acme", ":doc": "Document" }
想讓索引只攜帶文件、完全跳過篩選?只在文件項目上寫入
GSI1PK——這就是 稀疏索引。
沒有 GSI 鍵的項目永遠不會複製進索引,所以這次讀取只會碰到
文件。Type 屬性正是告訴你的寫入端哪些項目
符合資格的東西。
讓值保持穩定且單一
值一次選定,並把它當作一個列舉值。Document,絕不要有時 Doc
有時 document——一個飄移的值比沒有值更糟,因為你的
相等檢查會在某種大小寫上通過,卻悄悄漏掉另一種。
每個項目一個 Type。如果一個項目感覺像兩種實體,那通常是一種建模 氣味——它應該是兩個項目,各自在自己的集合或排序鍵範圍裡, 而不是一列身兼兩職。
遷移的回報
在需要之前就先烙上 Type 的理由是:重新建模。建議的 重新建模路徑是匯出、轉換、重新匯入——而 AWS 記載了批次匯出到 S3,正是為了這種離線重塑 (匯出 DynamoDB 到 S3)。
那一天到來時,Type 就是你 GROUP BY 的那一欄。想把留言
搬進它們自己的資料表,或把匯出重新正規化成每實體一檔的檔案、
給分析倉儲用?你就在 EntityType 上切分這份匯出。沒有它,你又得
回頭在數百萬列上逆向工程鍵。
後續步驟
Type 屬性是廉價的保險:在混合讀取中辨識實體、篩選 一個多載的 GSI,並在重新建模時乾淨地切分。從第一天起就在每次寫入 都烙上它——事後把它補進一張上線中的資料表,意味著一次完整回填。
延伸閱讀:單一資料表設計了解這個模式所
服務的混合分區;GSI 與 LSI了解如何選擇
稀疏索引背後的索引形狀;以及
Query 與 Scan了解為什麼 FilterExpression 永遠救不了
你的讀取成本。
用 DynamoDB 運算式建構器在 Type 上 建立篩選,並試用 DynoTable瀏覽一張真正的混合實體資料表, 看看 Type 欄如何在每個項目上對齊。