DynamoDB 中的鍵過載
從 SQL 過來的你,一個列永遠只表示一件事:orders.created_at 永遠是日期,users.email 永遠是郵箱。鍵過載把這套規則徹底拋棄。你給分割區索引鍵和起通用的名字——pk、sk——讓每種條目型別往裡灌注不同的含義。一張表,多種實體,一套結構。
DynamoDB 中的鍵過載是什麼?
鍵過載就是把多種實體型別用通用的鍵名(如 pk/sk)存進一張表,把型別編碼進值裡(USER#u_3001、INVOICE#2026-0014)。屬性名保持中性,於是使用者、發票和事件共享同一個分割區;值攜帶型別資訊,而排序索引鍵字首讓一次 Query 就能透過 begins_with 切出每種實體。
- 通用的鍵名,帶型別的值。把鍵命名為
pk/sk,把實體型別放進值裡:pk = "TENANT#acme",sk = "USER#u_3001"。名字是無意義的,值才攜帶型別。 - 它是單表設計能跑起來的關鍵。沒有過載,共享的一張表不過是個雜物抽屜。有了它,每個實體都落在一個你能
Query的分割區裡。 begins_with就是回報。排序索引鍵上的型別字首讓一次Query就能拉取整個實體,或它的一個切片,不需要Scan,也不需要過濾。- 代價是:可讀性。一份原始的
pk/sk轉儲什麼都告訴不了你。你需要一個能解碼字首的檢視器,否則就只能眯著眼盯著一堆字串。
為什麼通用名字勝過真實名字
DynamoDB 每張表最多給你兩個鍵屬性,而一次 Query 只能針對單個分割區索引鍵。所以如果你把鍵命名為 userId,那只有使用者條目才能乾淨地存進這張表——其他一切都得偽造一個 userId,或者搬到自己單獨的表裡去。
過載規避了這個問題。像 pk 這樣中性的名字不繫結任何實體,於是一個使用者、一張發票、一條審計事件都能共享同一個鍵屬性和同一張表。是值、而不是屬性名,說明了條目是什麼。
正是這一招,把單表設計從理論變成了你真正能查詢的東西。共享的表是容器;過載則是讓不同實體在其中共存的機制。
一個多租戶示例
假設你營運一個 SaaS 計費產品。每個租戶都有成員、發票和一份審計軌跡。與其用三張表,不如全部放進一張表並過載鍵:
| pk | sk | attributes |
|---|---|---|
| TENANT#acme | META | name="Acme Inc", plan="team" |
| TENANT#acme | USER#u_3001 | email, role="admin" |
| TENANT#acme | USER#u_3002 | email, role="member" |
| TENANT#acme | INVOICE#2026-0014 | amount_cents, status="paid" |
| TENANT#acme | INVOICE#2026-0015 | amount_cents, status="open" |
| TENANT#acme | EVENT#2026-06-23T09:12Z | actor="u_3001", action="invite" |
每一行都共享 pk = "TENANT#acme",因此它們構成一個——全部聚在一起,全部可在一次分割區讀取中觸及。
排序索引鍵字首在幹真正的活。它既給實體分組,又給它們排序。
查詢過載的集合
因為型別就存在排序索引鍵字首裡,begins_with 無需掃描任何東西就能按實體切分分割區:
Query pk = "TENANT#acme" -- the entire tenant, every type
Query pk = "TENANT#acme" AND begins_with(sk, "USER#") -- just members
Query pk = "TENANT#acme" AND begins_with(sk, "INVOICE#") -- just invoices
你只為條件匹配到的條目付費,而不是整個分割區——這與帶過濾的 Scan 恰好相反,後者你得為讀取那些隨後又被丟棄的行付費。AWS 把這叫作鍵條件;它在任何資料離開分割區之前先作用在鍵上。
如果你手工構造那個 begins_with 條件,一定要把型別標籤寫對——一個寫歪了的 USERS#(而非 USER#)會靜默地返回空結果。運算式構建器會生成 KeyConditionExpression 和 ExpressionAttributeValues 對映,讓字首與你真正寫的內容一致。
索引也一起過載
同樣的技巧適用於 。給它起通用的鍵名——gsi1pk、gsi1sk——讓每個實體寫入它需要的任何內容。這樣一個索引就能回答基表回答不了的模式。
| pk | sk | gsi1pk | gsi1sk |
|---|---|---|---|
| TENANT#acme | INVOICE#2026-0015 | STATUS#open | 2026-06-30 |
| TENANT#acme | INVOICE#2026-0014 | STATUS#paid | 2026-06-12 |
| TENANT#beta | INVOICE#2026-0099 | STATUS#open | 2026-06-25 |
現在 Query gsi1 WHERE gsi1pk = "STATUS#open" 會列出所有租戶中每一張待處理的發票,並按到期日排序——這是一個跨分割區檢視,基表那些以租戶為範圍的鍵永遠無法服務。另一個實體可以用它自己的含義複用 gsi1(比如 gsi1pk = "ROLE#admin"),所以一個索引就覆蓋了好幾種讀取。只是要記住,GSI 是最終一致的——它的寫入會滯後於基表。
在 DynoTable 中操作
原始的過載鍵讀起來很不友好:INVOICE#2026-0015 和 EVENT#2026-06-23T09:12Z 在扁平列表裡混作一團。一個按分割區分組、把字首顯式呈現的檢視器,能把雜物抽屜重新變回一個個實體。

陷阱
- 分隔符一次定好,永不更改。
#是約定俗成的選擇。在不同實體間混用#和:會以某種沒有任何東西會警告你的方式破壞begins_with。 - 別過載需要做範圍數學的值。排序索引鍵
INVOICE#2026-0015是按字典序排序的,不是按數值——給 id 並使用 ISO-8601 日期,好讓字串順序與你想要的順序一致。 - 給字首名稱空間留出預留位。兩個都以
USER開頭的實體型別(比如USER#和USERGROUP#)會在begins_with(sk, "USER")下發生衝突。要讓字首從第一個字元起就沒有歧義。 - 先規劃讀取,再定鍵。過載服務的是你已經列舉好的訪問模式。如果你還不知道自己的讀取需求,請先看單表設計——鍵是由查詢衍生出來的。
先規劃好一個分割區,然後下載 DynoTable 去瀏覽你自己的過載鍵,看著一次 Query 把整個租戶一次性拉回來。
Query 過載分割區的成本
列出 TENANT#acme 下的每個成員
begins_with(sk, "USER#") 僅讀取使用者行 — 不讀取發票或事件 —
因為關鍵條件在資料離開分割區之前進行過濾。在一個租客身上有 200 個使用者(每個 2 KB)和 5,000 個稽核事件(每個 1 KB),該查詢涉及約 400 KB(約 100 個最終一致的 RCU)。整個桌子上有Scan
要找到使用者,就要對每個租戶中的每個項目進行計量。將代表性的超載項目貼上到
item-size calculator,然後估算列表查詢pricing calculator。
使用單表工具進行設計
輸入實體(租戶、使用者、發票、事件)和訪問模式(“列出使用者對於租戶”、“跨租戶開具發票”)
single-table design tool。它建議與您將使用的過載字首匹配的 pk/sk 模板和 GSI 鍵在生產中 — 在您提交 CloudFormation 之前。
從模式發出查詢
固定字首後,在中構建關鍵條件
expression builder 並匯出分頁來自query builder的節目。字首拼寫錯誤
(USER# vs USERS#) 返回空集且沒有錯誤 — 生成的運算式減少無聲故障模式。
實體型別字首登入檔
維護一個簡短的內表開發者可以參考:
| 實體 | 排序字首 | 示例 SK | Query 切片 |
|---|---|---|---|
| 租戶元 | META | META | 單品獲取 |
| 使用者 | USER# | USER#u_3001 | begins_with(sk, "USER#") |
| 發票 | INVOICE# | INVOICE#2026-0015 | begins_with(sk, "INVOICE#") |
| 活動 | EVENT# | EVENT#2026-06-23T09:12Z | 具有降序讀取的按時間順序的尾部 |
新實體型別必須選擇在 begins_with 下不發生衝突的字首現有字首 - USER# 和 USERGROUP# 都匹配 begins_with(sk, "USER")除非你小心地延長或分隔。


