中階閱讀時間 1 分鐘

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_withbetween><——設計鍵時要讓你需要的那次讀取是一個前綴或一個範圍,而不是一次 Scan
  • 分隔符是承重的。 挑一個不可能出現在路徑片段裡的,否則兩個不相干的分支會碰撞。

為什麼排序鍵是整場遊戲的關鍵

從 SQL 過來,你會用一個 parent_id 自連接來建模資料夾樹,再遞迴地走過它——每一層一次查詢。在 DynamoDB 裡,那是對一個沒有 join 的鍵值儲存所擺出的 N+1 地雷。

DynamoDB 把每個項目存在某個分割鍵之下,依它的排序鍵排序,字串則以 UTF-8 位元組順序(AWS:Query 鍵條件)。所以如果你的排序鍵就是那條路徑,實體佈局就已經吻合那棵樹。一次讀取變成在一段連續切片上的前綴掃描——而不是一次圖走訪。

那就是那個轉變:排序鍵不是你要精確比對的識別符。它是一個可排序的位址。設計好它,查詢就免費掉出來了。

建模一棵檔案系統樹

假設你在儲存每個帳號的檔案樹。每個帳號一個磁碟是自然的分割;磁碟裡的路徑是排序鍵。

PKSKnode_typebytes
DRIVE#a91root/folder-
DRIVE#a91root/photos/folder-
DRIVE#a91root/photos/2026/folder-
DRIVE#a91root/photos/2026/beach.jpgfile284910
DRIVE#a91root/photos/2026/sunset.jpgfile512004
DRIVE#a91root/docs/folder-
DRIVE#a91root/docs/taxes.pdffile88210

這裡有兩個原創慣例在做事:

  • 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.jpgsunset.jpg——依路徑順序,在一次計費的讀取裡。你只為那段切片裡的項目付費,不是整個磁碟。

在 DynoTable 中,你直接對路徑排序鍵執行這個 begins_with 查詢,資料夾及其後代就會依路徑順序回傳——無需手寫佔位符語法。

需要為自己的程式碼準備原始的 KeyConditionExpression(名稱、值與 begins_with)?在 DynamoDB Expression Builder 裡建立並複製即可。

在 DynoTable 裡對路徑排序鍵跑一個 begins_with 查詢,依路徑順序回傳一個資料夾及其後代。
在 DynoTable 裡對路徑排序鍵跑一個 begins_with 查詢,依路徑順序回傳一個資料夾及其後代。

只列一層,不是整棵子樹

begins_with 給你的是遞迴讀取。若要一次非遞迴的目錄列出——root/photos/ 的直接子項目,不再更深——就存一個深度屬性並加一個排序鍵範圍外加一個過濾,或把路徑拆進一個 parent GSI。最單純的版本:保留一個 parent 屬性(root/photos/)並用它設一個 GSI 的鍵。

重點是:排序鍵便宜地回答前綴範圍問題。「只要直接子項目」是另一個問題——把它明確地建模出來,別指望一個 FilterExpression 會讓它有效率。過濾在讀取之後才執行,而它丟掉的每一個項目你都要付費。

小心挑選分隔符

分隔符是你資料契約的一部分。兩條規則:

  • 它絕不能出現在某個路徑片段裡。 如果檔名可以含 /,那 / 就是錯的分隔符——一個名為 a/b 的檔案,跟一個裝著 b 的資料夾 a 無從分辨。挑一個保留位元組(有些團隊用 # 或一個控制字元),並在片段裡禁用它。
  • 留意邊界處的排序順序。 /(0x2F)排在數字和字母之前,這通常正是你要的樹順序。換掉分隔符你就換掉了順序——用真實資料驗證它。

複合排序鍵 vs. 一個獨立的排序屬性

複合排序鍵(root/photos/2026/x純 ID 排序鍵 + parent 屬性
子樹讀取一次 begins_with 查詢遞迴查詢(N+1)或一次 GSI 走訪
排序路徑順序,免費必須加一個明確的排序屬性
移動/改名改寫所有後代更新一個 parent 指標
列直接子項目需要深度屬性或 GSI天然(parent = x

當讀取是子樹形狀且順序重要時,複合鍵勝出;當樹不斷變動時,扁平 ID 模型勝出。大多數讀取繁重的階層——檔案樹、分類樹、組織圖——都偏向複合。

陷阱與下一步

  • 別把鍵塞太滿。 你編碼進去的一切都是不可變的,而且只能依前綴索引。你要用相等比對來查的屬性,屬於它們自己的欄位或一個 GSI,不該硬塞進排序鍵裡。
  • 排序鍵做不了任意的 WHERE 只有 begins_withbetween 與比較。如果你發現自己在伸手抓一個 FilterExpression,那你大概把鍵建模錯了——見 Query 與 Scan
  • 更深入的鍵設計單表設計裡;至於什麼時候子樹讀取需要一個索引而非基礎表,見 GSI 與 LSI

Expression Builder 建好 begins_with 鍵條件,然後下載 DynoTable,對你自己的表執行這些前綴查詢,看著一棵子樹依路徑順序回來。

已更新