进阶阅读约 3 分钟

DynamoDB 筛选策略

DynamoDB 里的“筛选”指的是披着同一个词的四件不同的事。其中三件在数据被读取并计费 之前 缩小它;一件 —— 那个名叫 Filter 的 —— 在 之后 缩小它。知道哪个是哪个,就是这门手艺的大半。

DynamoDB 里的筛选是怎么工作的?

DynamoDB 有四种筛选方式,而只有一种在你被计费 之后 才运行。分区键挑出一个分区,排序键缩小到一段切片,稀疏索引按属性是否存在来筛选 —— 这三种都在计量之前削减你的读取成本。FilterExpression 在读取之后运行,所以它收缩响应,却从不收缩账单。

  • 分区键是最便宜的筛选:它挑出分区,所以你永远不碰表的其余部分。
  • 排序键begins_withbetween<> 在一个分区 内部 筛选 —— 仍然在计费之前,仍然便宜。
  • 稀疏索引靠缺席来筛选:一个项只有在拥有被索引的属性时才出现在索引里,所以索引 就是 那个筛选过的集合。
  • FilterExpression 是陷阱:它在 DynamoDB 计量读取之后运行,所以它削减你的响应大小,却从不削减你的账单。

搭起示例

一个产品目录。一张表,分区键 PK,排序键 SK

PK = "DEPT#kitchen"   SK = "PROD#00194"

每件产品还携带 priceinStock(一个布尔值)和 clearanceAt(一个 unix 时间戳,只在被标记清仓的项上存在)。一个部门里的各项共享一个分区,按产品 id 排序。

我们想要四种访问模式。每一种都映射到一种不同的筛选策略 —— 而其中任何一种上选错了,都是一次你要永远付费的 Scan

按分区键筛选

“给我 kitchen 里的每件产品。”分区键直接回答这个:

Query  PK = "DEPT#kitchen"

DynamoDB 恰好读取一个分区。表里别的东西都不会被触碰或计费。这是唯一一个在真正要紧的意义上免费的筛选 —— 它是 QueryScan 之间的差别。

从 SQL 过来,这感觉是反着的:没有一个 WHERE department = 'kitchen' 去扫描索引,你就只是 点名那个分区。如果你点名不了它,那是个建模问题,而非查询问题。

按排序键筛选

“给我 PROD#00100 往上的 kitchen 产品。”排序键在分区 内部 缩小,而且它在读取被计量之前就这么做:

Query  PK = "DEPT#kitchen"  AND  SK between "PROD#00100" AND "PROD#00200"

排序键条件是有意受限的:=<<=>>=betweenbegins_with。没有 OR,没有任意谓词。

那个约束就是让读取保持有针对性的东西 —— DynamoDB 遍历一段连续的切片,而非整个分区。

这里的杠杆是你怎么编码排序键。如果你的模式是“按价格区间”,那么一个 PROD#<id> 排序键帮不上忙 —— 你得把价格烤进键里。

那是一个排序键策略的决定,在设计时做出,而非查询时。

按稀疏索引筛选

“给我当前在清仓的一切。”大多数产品不在,所以你不想为了找出那少数几个而读整个目录。

一个稀疏索引靠缺席解决这个。一个全局二级索引只有在某个项 两个 索引键属性都拥有时才包含它。

把 GSI 分区键设为 clearanceAt —— 只在清仓项上存在 —— 索引就别的什么都不容纳。

AWS 把这点讲明白了:一个 GSI “只包含拥有被索引属性的项”,所以缺少键属性的项干脆不被传播过去(AWS —— 利用稀疏索引)。

基表 —— 所有产品 clearanceAt 吗?复制到 ClearanceIndex不在索引中查询索引 = 仅清仓项

现在查询只读取清仓项,只为它们计费:

Query  ON ClearanceIndex   GSI_PK = "CLEARANCE"   (sorted by clearanceAt)

筛选发生在你 写入 数据时 —— 通过选择是否设置 clearanceAt。索引就是那个筛选过的集合。参见 GSI 对比 LSI 看哪种索引类型合适。

用 FilterExpression 筛选

“给我有库存的 kitchen 产品。”inStock 不是一个键属性,所以你去够一个 FilterExpression

Query  PK = "DEPT#kitchen"
Filter inStock = true

陷阱来了。DynamoDB 读取 kitchen 分区里的每一个项、为它们全部计量容量,然后 丢掉无库存的那些。

官方规则:一个筛选表达式“在一次 Query 完成之后、但在结果被返回之前应用”,并且“不消耗任何额外的读取容量单元” —— 你已经为完整的那次读取付过费了(AWS —— Query 的筛选表达式)。

所以如果 kitchen 有 10,000 件产品而 12 件有库存,你要为读取 10,000 件付费。响应很小;账单不小。FilterExpression 收缩跨过网线的负载,从不收缩读取。

还有第二条更锋利的边:分页是在筛选 之前 计量的。一页是 1 MB 的被读取项,而非 1 MB 的匹配项。

一个筛选可以返回一个设置了 LastEvaluatedKey 的空页 —— DynamoDB 读了完整的一兆字节、什么都没匹配、交给你一个空数组。你继续翻页,而你为每一个空页付了费。

DynamoDB 表达式构建器构建这个表达式 —— 名称、值和正确的保留字转义 —— 好让 #inStock/:val 占位符第一次就正确。

四者对比

何时筛选削减读取成本?谓词能力搭建成本
分区键读取前是 —— 一个分区仅相等免费(它就是键)
排序键读取前是 —— 一段切片范围 / begins_with排序键设计
稀疏索引读取前是 —— 仅索引某个属性的存在额外的 GSI + 写入成本
**FilterExpression读取后否**几乎任何条件

从上往下读这张表:谓词能力 上升,成本控制 下降FilterExpression 能精确地表达任何东西,正因为它在已读取的项上运行 —— 那也正是它省不了你钱的同一个原因。

在 DynoTable 中看见它

当你运行一个带筛选的 Query 时,被 读取 的项与被 返回 的项之间的差距就是整个故事。DynoTable 把已消耗容量摆在结果计数旁边 —— 于是一个悄悄读了整个分区的筛选是可见的,而非藏在你的月账单里。

对于筛选回答不了的真正跨项问题 —— “每个部门的平均价格”、“有库存的产品连接到它们的评论” —— DynoTable 的 SQL Workbench 在一个有界的结果集上客户端地运行 GROUP BYJOIN 和聚合,而非编译成一次全表 Scan

暗坑与下一步

  • 别把 FilterExpression 当作你的主访问路径。 如果一个模式很常见,把它建模进一个键或一个稀疏索引。筛选是用来做最后那点收尾缩小的,而非其中的大头。
  • 留意空页。 一个带筛选的查询可能长时间翻页却什么也不返回。尊重 LastEvaluatedKey;别以为一个空页就意味着“完了”。
  • 稀疏索引不是免费的。 它为每一个落进它的项花费写入容量和存储 —— 属性稀有时便宜,不稀有时就没那么便宜了。

容量计算器估算一次带筛选的读取实际会花多少,并试用 DynoTable,对着你自己的表盯住已消耗容量与返回行数的对比。

更新于