DynamoDB 中的複合排序索引鍵
是一個分割區索引鍵加上一個排序索引鍵。讓它真正強大的訣竅,在於你往排序索引鍵 裡 放了什麼:把一個層級結構編碼成一個帶分隔符的字串,一次 Query 就能按排序順序讀出整棵子樹——沒有 join、沒有遞迴、沒有第二次往返。
DynamoDB 中的複合排序索引鍵是怎麼工作的?
複合排序索引鍵把一個層級結構打包進一個帶分隔符的字串——root/photos/2026/——DynamoDB 按 UTF-8 位元組順序儲存它。因為這個佈局本身就與樹對應,一次帶 begins_with(SK, "root/photos/") 的 Query 就能按路徑順序讀出整棵子樹。沒有 join、沒有遞迴、沒有第二次往返——只是對一個連續的 做一次字首掃描。
- 排序索引鍵是一個可排序的字串,不只是一個 ID。 把路徑打包進去——
root/photos/2026/——DynamoDB 會自動按 UTF-8 位元組順序儲存該分割區內的項。 - 一個分隔符把字首匹配變成子樹讀取。
begins_with(SK, "root/photos/")在一次查詢裡就返回那個資料夾的每一個後代。 - 排序索引鍵支援範圍條件,而非任意篩選。 你能用的是
begins_with、between、>、<——把鍵設計成讓你需要的讀取是一個字首或一個範圍,而不是一次Scan。 - 分隔符是承重的。 挑一個不可能出現在路徑段裡的分隔符,否則兩條不相干的分支會撞車。
為什麼排序索引鍵就是全部的關鍵
從 SQL 過來,你會用一個 parent_id 自連線來建模資料夾樹,然後遞迴地遍歷它——每一層一次查詢。在 DynamoDB 裡,這是對著一個沒有 join 的鍵值儲存埋下的 N+1 暗雷。
DynamoDB 把每個項儲存在某個分割區索引鍵之下,按其排序索引鍵排序,字串按 UTF-8 位元組順序排列(AWS:Query 鍵條件)。所以如果你的排序索引鍵 就是 路徑,那麼物理佈局本身就與樹對應。一次讀取就變成對一個連續切片的字首掃描——而不是一次圖遍歷。
這就是那個轉變:排序索引鍵不是你要精確匹配的一個識別符號。它是一個可排序的地址。把它設計好,查詢就白送出來了。
建模一棵檔案系統樹
假設你在儲存每個帳戶的檔案樹。一個帳戶一個盤是天然的分割區;盤內的路徑就是排序索引鍵。
| PK | SK | node_type | bytes |
|---|---|---|---|
| DRIVE#a91 | root/ | folder | - |
| DRIVE#a91 | root/docs/ | folder | - |
| DRIVE#a91 | root/docs/taxes.pdf | file | 88210 |
| DRIVE#a91 | root/photos/ | folder | - |
| DRIVE#a91 | root/photos/2026/ | folder | - |
| DRIVE#a91 | root/photos/2026/beach.jpg | file | 284910 |
| DRIVE#a91 | root/photos/2026/sunset.jpg | file | 512004 |
這裡有兩個原創的約定在起作用:
PK = DRIVE#<account>把一個帳戶的整棵樹保持在單個 裡,所以任何子樹讀取都是一次單分割區Query。SK是完整路徑,資料夾後面帶一個尾部/。這個尾部斜槓是刻意的——它讓一個資料夾排在它自己的子項 之前,並讓root/photos/與一個名為root/photos的同級檔案區分開。
一次查詢讀出一棵子樹
列出 root/photos/ 之下的一切——資料夾、子資料夾和檔案,遞迴地:
Query
KeyConditionExpression = PK = :drive AND begins_with(SK, :prefix)
:drive = "DRIVE#a91"
:prefix = "root/photos/"
這會返回 root/photos/、root/photos/2026/、beach.jpg 和 sunset.jpg——按路徑順序,在一次計費讀取裡。你只為那個切片裡的項付費,而不是整個盤。
在 DynoTable 裡,你對著路徑排序索引鍵執行的正是這個 begins_with 查詢,資料夾連同它的後代就按路徑順序回來了——不用手寫任何預留位置語法。
需要為你自己的程式碼拿到原始的 KeyConditionExpression(名稱、值和 begins_with)?在 DynamoDB 運算式構建器 裡構建並複製它。

只列一層,而不是整棵子樹
begins_with 給你的是 遞迴 讀取。要做一次非遞迴的目錄列舉——只要 root/photos/ 的直接子項、不再往深裡去——就存一個 depth(深度) 屬性,加一個排序索引鍵範圍加一個篩選,或者把路徑拆到一個 parent GSI 裡。最簡單的版本:保留一個 parent 屬性(root/photos/),並建一個以它為鍵的 GSI。
要點在於:排序索引鍵能廉價地回答 字首 和 範圍 問題。“只要直接子項”是一個不同的問題——顯式地為它建模,而不是指望一個 FilterExpression 能讓它變高效。篩選在讀取 之後 執行,而它丟棄的每一個項你都要付費。
謹慎地挑選分隔符
分隔符是你資料契約的一部分。兩條規則:
- 它絕不能出現在某個路徑段內部。 如果檔名可以包含
/,那麼/就是錯誤的分隔符——一個名為a/b的檔案與一個裝著b的資料夾a無法區分。挑一個保留位元組(有些團隊用#或一個控制字元),並在路徑段裡禁用它。 - 留意邊界處的排序順序。
/(0x2F)排在數字和字母之前,這通常正是你想要的樹序。換了分隔符就換了排序——用真實資料驗證它。
複合排序索引鍵 對比 一個單獨的排序屬性
複合排序索引鍵(root/photos/2026/x) | 純 ID 排序索引鍵 + parent 屬性 | |
|---|---|---|
| 子樹讀取 | 一次 begins_with 查詢 | 遞迴查詢(N+1)或一次 GSI 遍歷 |
| 排序 | 路徑順序,白送 | 必須加一個顯式的排序屬性 |
| 移動 / 重新命名 | 重寫所有後代 | 更新一個 parent 指標 |
| 直接子項列舉 | 需要 depth 屬性或 GSI | 天然(parent = x) |
當讀取是 子樹形狀且排序有意義 時,複合鍵勝出;當樹在不斷變動時,扁平 ID 模型勝出。大多數讀密集的層級結構——檔案樹、分類樹、組織架構圖——都偏向複合。
陷阱與後續步驟
- 不要往鍵裡塞太多。 你編碼進去的一切都是不可變的,且只按字首索引。你要按等值查詢的屬性屬於它們自己的欄位或一個 GSI,而不是硬塞進排序索引鍵。
- 排序索引鍵做不了任意的
WHERE。 只有begins_with、between和比較。如果你發現自己在伸手去用FilterExpression,你多半把鍵建模錯了——參見 Query 對比 Scan。 - 關於鍵設計的更深入內容 見 單表設計;至於什麼時候一次子樹讀取需要一個索引而非基表,參見 GSI 對比 LSI。
用 運算式構建器 構建 begins_with 鍵條件,然後 下載 DynoTable,對著你自己的表執行這些字首查詢,看著一棵子樹按路徑順序回來。(而你在 SQL 裡留下的那個自 JOIN?需要時 DynoTable 的 SQL Workbench 依然能執行它。)


