中階閱讀時間 3 分鐘

DynamoDB 中的 Type 屬性

在 SQL 中,一行所在的表_就是_它的型別——documents 表裡的一行就是一個文件。而 DynamoDB 單表把所有實體混在同一套 schema 下,因此一個項目本身並不能回答「這是什麼?」這個問題。

Type 屬性把答案補了回來:它是每個項目上的一個普通字串,標明該項目所代表的實體。

DynamoDB 中的 Type 屬性是什麼?

Type 屬性是你在每個項目上打的一個普通字串——例如 EntityType: "Document"——用來標明該項目所代表的實體。由於單表把許多實體混在同一套 schema 下,項目本身並不帶內建的型別資訊。Type 把它補了回來,讓你的程式碼得以識別行、把 GSI 過濾到單一實體,並在遷移中存活下來。

  • 每次寫入都打上 Type。 每個項目上都有一個屬性——EntityType: "Document"——無一例外。它只花費幾個位元組,卻能在日後幫你省下大麻煩。
  • 它能在混合分割區中識別實體。 一次 Query 會把工作區、文件和評論一併返回;Type 讓你的程式碼無需解析鍵字首就能分辨誰是誰。
  • 它為上的單實體過濾提供了支撐。 把 Type 投影進索引,你就能把一個過載索引縮窄到恰好一種實體型別。
  • 它是遷移時的逃生艙。 當你匯出資料以重新建模、或把某個實體拆到獨立的表時,Type 就是你用來切分的那一列。

為什麼混合表會丟失型別

單表設計把每個實體都存進一張表,背後用 PKSK 這類通用鍵。這正是它的意義所在——一次 Query 就能把父項和它的子項一併返回。但這也意味著一個分割區是異構的。

以一個 SaaS 文件協作應用為例。一個工作區分割區裡存放著工作區記錄、它的文件,以及那些文件上的評論:

PKSKattributes
WS#acmeMETAname, plan, seats
WS#acmeDOC#a1#METAtitle, owner, wordCount
WS#acmeDOC#a1#CMT#0007author, body, createdAt
WS#acmeDOC#a1#CMT#0008author, body, createdAt

Query PK = "WS#acme" 會在一次計費讀取中把全部四個項目返回。現在你的程式碼拿到了一串原始項目,卻沒有可靠的辦法說清哪個是文件、哪個是評論——除非去對 SK 做字串匹配,而這在你的鍵格式一變時就會崩掉。

在每個項目上打上 Type

修復方法就是每次寫入都加上一個屬性,用來標明實體:

PKSKEntityTypetitle
WS#acmeMETAWorkspace
WS#acmeDOC#a1#METADocumentQ3 Roadmap
WS#acmeDOC#a1#CMT#0007Comment

基於 item.EntityType === "Document" 做分支是一個穩定的相等判斷。而解析 SK.startsWith("DOC#") && SK.includes("#CMT#") 只是一種猜測,一旦你改動鍵就會失靈。Type 讓你的讀取邏輯與鍵編碼解耦——這才是真正的收益。

Query PK = 'WS#acme'混合分区EntityType: 'Workspace'EntityType: 'Document'EntityType: 'Comment' Type 路由

一次讀取返回三種實體型別;Type 屬性把每個項目路由到正確的處理邏輯,完全無需觸碰鍵。

把 GSI 過濾到單一實體

Type 的價值在索引上體現得最充分。假設你新增一個 GSI,以 GSI1PK = WS#acmeGSI1SK = updatedAt 為鍵,用來列出「這個工作區裡最近改動過的所有內容,最新的排在最前」。一個過載索引會把文件_和_評論一併掃入——但一個資訊流 UI 可能只想要文件。

有兩種縮窄方式,而兩者的區別關乎花錢:

方式它的代價何時使用
對 Type 使用 FilterExpression讀取所有匹配的項目、按全部項目計費,再在讀取之後丟棄不匹配的結果中混入的實體很少見;追求快速上線
稀疏索引GSI1PK 只寫在目標實體上)只有你想要的那種實體才會進入索引某一種實體佔主導;你希望零浪費

us-east-1 的按需模式下,一次返回 100 個混合項目、每個 2 KB 的 GSI Query,按最終一致大約計 100 個 RCU——而對 EntityTypeFilterExpression 照樣會在丟掉評論之前把每一行都計量一遍。一個從不索引評論的稀疏索引,只為文件行計費。兩種形態都可以在定價計算器裡建模。

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 vs LSI幫你選擇稀疏索引背後的索引形態,以及 Query vs Scan講清為什麼 FilterExpression 永遠救不了你的讀取成本。

DynamoDB 運算式構建器基於 Type 構建過濾條件,然後試用 DynoTable去瀏覽一張真實的混合實體表,看看 Type 這一列如何在每個項目上對齊。

已更新