中階閱讀時間 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/docs/folder-
DRIVE#a91root/docs/taxes.pdffile88210
DRIVE#a91root/photos/folder-
DRIVE#a91root/photos/2026/folder-
DRIVE#a91root/photos/2026/beach.jpgfile284910
DRIVE#a91root/photos/2026/sunset.jpgfile512004

這裡有兩個原創的約定在起作用:

  • 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 運算式構建器 裡構建並複製它。

在 DynoTable 中對路徑排序索引鍵執行 begins_with 查詢,按路徑順序返回一個資料夾及其後代。
在 DynoTable 中對路徑排序索引鍵執行 begins_with 查詢,按路徑順序返回一個資料夾及其後代。

只列一層,而不是整棵子樹

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_withbetween 和比較。如果你發現自己在伸手去用 FilterExpression,你多半把鍵建模錯了——參見 Query 對比 Scan
  • 關於鍵設計的更深入內容單表設計;至於什麼時候一次子樹讀取需要一個索引而非基表,參見 GSI 對比 LSI

運算式構建器 構建 begins_with 鍵條件,然後 下載 DynoTable,對著你自己的表執行這些字首查詢,看著一棵子樹按路徑順序回來。(而你在 SQL 裡留下的那個自 JOIN?需要時 DynoTable 的 SQL Workbench 依然能執行它。)

已更新

互動式地試用這個設計

在免費的 DynamoDB 單一資料表設計工具中勾勒你的實體與存取模式 — 它會建議 PK/SK 鍵範本、預覽項目集合,並顯示哪些模式需要 GSI。

開啟單一資料表設計工具