入门阅读约 3 分钟

如何在 DynamoDB 中做 COUNT、SUM 与聚合

DynamoDB 只有一个内置聚合:用 Select=COUNT 统计匹配的 item 数量。它没有原生的 SUMAVGMINMAX。而且即便是这个你拿到的计数,也会读取(并对此计费)它统计过的每一个 item。本文涵盖实际支持的能力、人们退而求其次的各种近似做法,以及当你真的需要时,如何对一张表运行真正的 COUNT/SUM/AVG

DynamoDB 能做 SUM、COUNT 和聚合函数吗?

基本不能。DynamoDB 唯一内置的聚合是 Select=COUNT,它返回匹配的 item 数量,但仍然会读取(并对此计费)每一个 item。它没有原生的 SUMAVGMINMAX,PartiQL 也不会增加任何聚合。要获得带 GROUP BY 的真正聚合,请在你的应用中折叠计算、维护一个计数器,或在 DynoTable 的 Workbench 中运行 SQL

  • Select=COUNT 返回匹配的 item 数量,但 DynamoDB 仍然会读取每一个 item 才能得出它——你支付的是完整的 Scan/Query 读取成本,而不是一个便宜的“计数”成本。
  • 没有原生的 SUMAVGMINMAX DynamoDB 的读取操作返回 item;它们不会把这些 item 折叠成一个数字。PartiQL 也不会增加聚合。
  • DescribeTable.ItemCount 是免费的,但是近似值,而且只“大约每六小时”更新一次——用于仪表盘上的一个小卡片还行,但对任何需要精确的场景都是错的。
  • 要获得精确的 COUNT/SUM/AVG/MIN/MAX(带 GROUP BY),请在你的应用中聚合、维护一个计数器,或在 DynoTable 的 SQL Workbench(见下文)中运行。

统计 item 数量:Select=COUNT

QueryScan 都接受一个 Select 参数。把它设为 COUNT,响应中携带的就是计数而不是 item:

aws dynamodb scan \
  --table-name Orders \
  --select COUNT \
  --filter-expression "#s = :open" \
  --expression-attribute-names '{"#s":"status"}' \
  --expression-attribute-values '{":open":{"S":"OPEN"}}'

响应会给你两个数字(AWS:统计结果中的 item 数量):

  • Count ——“在应用过滤表达式(如果有)之后剩下的 item 数量。”
  • ScannedCount ——“在应用任何 ScanFilter 之前被评估的 item 数量。”没有过滤器时,ScannedCountCount 相同。

如果你只有分区键,需要统计其中的重复项,那么你传入的条件 + 过滤器,正是 DynamoDB Expression Builder 所生成的——上面的 KeyConditionExpressionFilterExpression 以及 ExpressionAttributeNames/Values 映射——而无需手工转义 JSON。

还有两个常坑大表统计者的陷阱:

  • 1 MB 的分页上限仍然适用。 “如果 Scan 结果集的大小超过 1 MB,ScannedCountCount 仅代表全部 item 的一个部分计数”(AWS Scan 文档)。你必须用 LastEvaluatedKeyExclusiveStartKey 分页,并保持一个累计值,才能得到真实的数字——这就是 DynamoDB 分页中讲到的那个循环。
  • 一个窄范围的 Query 胜过 ScanQuery 使用 Select=COUNT 只会计量目标分区中的 item,而不是整张表。如果你能锁定一个分区键(基表或某个 GSI),就在那里计数——这就是把 Query 与 Scan 的成本差距应用到了计数上。

Select=COUNTItemCount(以及为什么它会过时)

DescribeTable 免费返回一个 ItemCount(以及 TableSizeBytes),不产生读取成本。问题就出在 API 参考本身:“DynamoDB 大约每六小时更新一次这个值。最近的更改可能不会反映在这个值中。”所以它可能远远落后于你这张表的实际状态。

Select=COUNTDescribeTable.ItemCount
精确度精确(针对匹配集)近似
新鲜度实时约每 6 小时更新
成本读取并对每个计数的 item 计费免费(元数据)
能否过滤 / 统计子集能(过滤表达式)不能——仅整张表

ItemCount 来粗略地“这张表有多大”地估摸一下,或者用于仪表盘卡片。当你需要一个精确、过滤后或当前的数字时用 Select=COUNT——并接受其读取成本。对于任何真正实时且免费的需求,自己维护一个计数器(见下文聚合模式)。

为什么没有原生的 SUM/AVG/MIN/MAX

DynamoDB 的读取操作返回 item。没有查询规划器能把结果集折叠成一个标量,所以也就没有什么可以用来计算 SUMAVG。计数是 API 提供的唯一一种折叠,通过 Select=COUNT 实现。

PartiQL 不会改变这一点。PartiQL 的 SELECT 语法SELECT {{expression}} [, …] FROM {{table}}[.{{index}}] [WHERE …] [ORDER BY {{key}} …],其中 expression 是“由 * 通配符或由一个或多个属性名或文档路径组成的投影列表所构成的投影”。该语法中没有聚合函数,也没有 GROUP BY 子句——而 ORDER BY 接受一个 {{key}},文档将其描述为“用于对返回结果排序的哈希键或排序键”。每一条 PartiQL 的 SELECT 仍然编译为一个 GetItemQueryScan,所以 SELECT SUM(total) FROM "Orders" 根本就无法表达。(关于 PartiQL 的上限,详见 PartiQL 与 SQL。)

聚合模式(计数器、流、应用侧)

既然 DynamoDB 不会替你聚合,那些既有的模式就把工作推到别处:

  • 维护一个计数器 item。 保留一个专用 item(例如 PK = "STATS#orders"),并在每次写入时用 UpdateItem 对一个数值属性执行 ADD。这样读取聚合值就是一次 GetItem——精确且便宜,但增量逻辑、其一致性以及当某个计数器被频繁打到时的争用,都得你自己负责。
  • DynamoDB Streams → 聚合器。 启用一个流并把它接到一个 Lambda,让它在 item 变化时更新累计值(计数、求和)。根据 AWS Streams 文档,你可以配置流的 StreamViewType,让每条记录携带 NEW_AND_OLD_IMAGES——“item 的新映像和旧映像”——足以在不重新扫描的情况下保持 SUM 式聚合的最新。流记录有 24 小时的生命周期约束(“一个分片中的流记录在 24 小时后会被自动移除”),所以消费者必须跟得上。
  • 应用侧折叠。 翻页遍历匹配的 item,并在你自己的代码里累加 SUM/AVG/MIN/MAX。正确,但它每次都读取(并对此计费)每一个 item——与 Select=COUNT 相同的成本画像,再加上数据传输。
  • 下放到分析系统。 对于繁重或临时的分析型聚合,把表导出到 S3 并用 Athena 查询,或把它流式导入一个数据仓库。根据 AWS 导出到 S3 文档,导出“不消耗读取容量单元”,并让你“使用诸如 Athena 这样的 AWS 服务执行分析和复杂查询”——这是当你的需求超出按请求聚合之后,AWS 推荐的路径。

每一种都在用简单性换取要么是写入时的簿记开销(计数器、流),要么是读取时的成本(应用侧扫描)。没有哪种模式能让 DynamoDB 自己免费算出一个 SUM。这种权衡的分组版本——按每个键而不是对整张表聚合——是它自己的一篇指南:DynamoDB GROUP BY

在 DynoTable 的 SQL Workbench 中运行 COUNT/SUM/AVG

当你只是需要答案——“有多少个 OPEN 订单,它们的总额是多少”——而不想写一个分页的扫描循环或一个 Lambda 时,DynoTable 的 SQL Workbench 会运行真正的聚合。它通过 DynamoDB 实际的 Query/Scan 运行时把你的表物化出来,然后在其上运行你完整的 SQL:在 DynamoDB 访问模式规则之内的 SQL。

-- Runs in the DynoTable Workbench (NOT in PartiQL):
SELECT status,
       COUNT(*)        AS orders,
       SUM(total)      AS revenue,
       AVG(total)      AS avg_order,
       MIN(total)      AS smallest,
       MAX(total)      AS largest
FROM orders
GROUP BY status
ORDER BY revenue DESC

这就是 COUNTSUMAVGMINMAXGROUP BYORDER BY——这些 DynamoDB 或 PartiQL 都无法表达——全在一条语句里。这与 SQL for DynamoDB 是同一个分析切入点;完整的分组故事请见 DynamoDB GROUP BY

Workbench 对底层的访问模型是诚实的,不是一个假装的 Postgres:

  • 行仍然经由 DynamoDB 真实的 Query/Scan 而来。对整张表的 GROUP BY 底层依然是一次 Scan——Workbench 把这个成本暴露出来,而不是把它藏起来,这与 Query 与 Scan 是同一个权衡。
  • 聚合在行落地之后,作用于物化出来的标量属性。

常见问题

我能不扫描就统计 DynamoDB 中的 item 数量吗? 不完全能。要得到精确、当前的计数,你必须读取这些 item——Select=COUNT 仍然会计量每一个被计数的 item。唯一无需扫描的选项,是近似的 DescribeTable.ItemCount(约每 6 小时更新一次),或者一个你在每次写入时自己维护的计数器 item。

我如何按 GSI 统计 item 数量? 对索引运行带 Select=COUNTQuery(或 Scan)。通过一个窄范围的 GSI 分区来计数,远比扫描基表便宜,因为你只读取该索引分区中的 item——围绕你需要的计数来对索引建模。

DescribeTable.ItemCount 准确吗? 它是近似的。API 参考指出 DynamoDB“大约每六小时”更新一次 ItemCountTableSizeBytes,并且“最近的更改可能不会反映在这个值中”。在需要精确或实时数字的地方,不要使用它。

DynamoDB 能做 SUMAVG 吗? 原生不能,在 PartiQL 中也不能——PartiQL 的 SELECT 语法没有聚合函数。请在你的应用中聚合、维护一个计数器(可选地通过 DynamoDB Streams),或在 DynoTable 的 SQL Workbench 中运行 SUM/AVG

CountScannedCount 有什么区别? ScannedCount 是 DynamoDB 在你的过滤之前评估了多少个 item;Count 是过滤之后剩下多少个。没有过滤表达式时两者相等。两者之间差距很大,意味着这是一次低效的计数。


需要对你的 DynamoDB 数据求和、求平均或分组,又不想写扫描循环?下载 DynoTable 并在一个 Workbench 标签页中运行它。还在对比客户端?看看它与普通 DynamoDB GUI 相比落在哪里。

更新于