进阶阅读约 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。

打开单表设计工具