如何在 DynamoDB 中做 COUNT、SUM 和聚合
DynamoDB 恰好只有一个内置聚合:用 Select=COUNT 计数匹配的项。没有原生的 SUM、AVG、MIN 或 MAX。而且即便是你 能 拿到的那个计数,也要读取(并计费)它计入的每一个项。本指南讲清楚哪些是真正受支持的、人们会去够的那些近似做法,以及当你需要时如何对着一张表运行真正的 COUNT/SUM/AVG。
DynamoDB 能做 SUM、COUNT 和聚合函数吗?
大体上不能。DynamoDB 唯一的内置聚合是 Select=COUNT,它返回匹配项的计数,但仍然会读取(并计费)每一个项。没有原生的 SUM、AVG、MIN 或 MAX,PartiQL 也一个都没加。要做带 GROUP BY 的真正聚合,就在你的应用里折叠它们、维护一个计数器,或者在 DynoTable 的 Workbench 里运行 SQL。
Select=COUNT返回匹配项的数量,但 DynamoDB 仍然要读取每一个项来算出它——你付的是完整的Scan/Query读取成本,而不是一个廉价的“计数”成本。- 没有原生的
SUM、AVG、MIN或MAX。 DynamoDB 的读取操作返回项;它们不会把项折叠成一个数字。PartiQL 也没加聚合。 DescribeTable.ItemCount是免费的,但 是近似的,且只“大约每六小时”更新一次——对一个仪表盘的小方块来说没问题,对任何需要精确的场景来说都是错的。- 要精确的
COUNT/SUM/AVG/MIN/MAX(带GROUP BY),就在你的应用里聚合、维护一个计数器,或者在 DynoTable 的 SQL Workbench 里运行它(见下文)。
计数项:Select=COUNT
Query 和 Scan 都接受一个 Select 参数。把它设成 COUNT,响应里带的就是计数而不是项:
aws dynamodb scan \
--table-name Orders \
--select COUNT \
--filter-expression "#s = :open" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":open":{"S":"OPEN"}}'响应给你两个数字(AWS:计数结果中的项):
Count——“在应用了筛选表达式(如果有的话)之后 剩下的项的数量。”ScannedCount——“在任何ScanFilter被应用 之前 被求值的项的数量。”没有筛选时,ScannedCount与Count相同。
如果你只有 ,需要计数其中的重复项,你所传入的那个条件 + 筛选,正是 DynamoDB 表达式构建器 生成的东西——上面那些 FilterExpression 和 ExpressionAttributeNames/Values 映射,加上当你通过 Query 在一个分区内计数时的 KeyConditionExpression——而无需手工转义 JSON。
还有两个坑,会咬到计数大表的人:
- 1 MB 分页上限仍然适用。 “如果
Scan结果集的大小大于 1 MB,ScannedCount和Count只代表全部项的部分计数”(AWS Scan 文档)。你必须分页——把每次响应的LastEvaluatedKey作为下一次请求的ExclusiveStartKey回喂进去,一路保留一个累加值来得到真实的数字——这就是 DynamoDB 分页 里讲的同一个循环。 - 一次窄的
Query胜过一次Scan。 在一次Query上用Select=COUNT只计量目标分区里的项,而不是整张表。如果你能锁定一个分区键(基表或一个 GSI),就在那里计数——这是把 Query 对比 Scan 的成本差应用到了计数上。
Select=COUNT 对比 ItemCount(以及为什么它是陈旧的)
DescribeTable 免费返回一个 ItemCount(和 TableSizeBytes),没有读取成本。问题在于 API 参考本身:“DynamoDB 大约每六小时更新这个值。近期的改动可能不会反映在这个值里。”所以它可能远远落后于你表的实际状态。
Select=COUNT | DescribeTable.ItemCount | |
|---|---|---|
| 精确性 | 精确(对匹配集而言) | 近似 |
| 新鲜度 | 实时 | 约每 6 小时更新 |
| 成本 | 读取并计费每一个被计数的项 | 免费(元数据) |
| 能否筛选 / 计数子集 | 能(筛选表达式) | 不能——只能整张表 |
用 ItemCount 来做一次粗略的“这张表有多大”的直觉判断,或者一个仪表盘小方块。当你需要一个精确的、筛选过的或当前的数字时用 Select=COUNT——并接受读取成本。至于任何真正实时且免费的东西,就自己跟踪一个计数器(见下面的 聚合模式)。
为什么没有原生的 SUM/AVG/MIN/MAX
DynamoDB 的读取操作返回项。没有查询规划器把一个结果集折叠成一个标量,所以没有东西可以用来算出一个 SUM 或 AVG。计数是这个 API 提供的唯一折叠,通过 Select=COUNT。
PartiQL 并不改变这一点。PartiQL SELECT 语法 是 SELECT {{expression}} [, …] FROM {{table}}[.{{index}}] [WHERE …] [ORDER BY {{key}} …],其中 expression 是“由 * 通配符或一个由一个或多个属性名或文档路径构成的投影列表形成的一个投影。”那个语法里没有聚合函数、没有 GROUP BY 子句——而 ORDER BY 取的是一个 {{key}},文档里说是“用来给返回结果排序的一个哈希键或一个排序键。”每一个 PartiQL SELECT 仍然编译成一个 GetItem、Query 或 Scan,所以 SELECT SUM(total) FROM "Orders" 根本就无法表达。(关于 PartiQL 天花板的更多内容见 PartiQL 对比 SQL。)
聚合模式(计数器、Streams、应用端)
既然 DynamoDB 不会替你聚合,那些成熟的模式就把这份活儿推到别处:
- 维护一个计数器项。 保留一个专门的项(例如
PK = "STATS#orders"),并在每次写入时用一个UpdateItem对一个数值属性ADD。读取这个聚合就变成一次GetItem——精确且廉价,但递增逻辑、它的一致性,以及某个计数器被猛击时的争用,都归你负责。 - 喂给一个聚合器。 启用一个流,把它接到一个 Lambda,在项变化时更新累计合计(计数、求和)。按照 AWS Streams 文档,你可以配置流的
StreamViewType,让每条记录携带NEW_AND_OLD_IMAGES——“项的新旧两个映像”——足以在不重新扫描的情况下让SUM式的聚合保持最新。流记录有 24 小时的生存期(“分片内的流记录在 24 小时后自动移除”),所以消费者必须跟得上。 - 应用端折叠。 分页遍历匹配的项,在你自己的代码里累积
SUM/AVG/MIN/MAX。正确,但它每次都读取(并计费)每一个项——与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 运行时把你的表物化出来,然后在其之上运行单条 SELECT——聚合、GROUP BY、HAVING、DISTINCT:SQL,在 DynamoDB 的访问模式规则之内。
-- 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这就是 COUNT、SUM、AVG、MIN、MAX、GROUP BY 和对一个计算聚合的 ORDER BY——其中没有一样是 DynamoDB 或 PartiQL 能表达的(PartiQL 的 ORDER BY 仅限于键属性)——就在一条语句里。这与 DynamoDB 的 SQL 是同一个分析型楔子;关于完整的分组故事,参见 DynamoDB GROUP BY。
Workbench 对底下的访问模型是诚实的,不是一个假装的 Postgres:
- 那些行仍然通过 DynamoDB 真正的 Query/Scan 而来。对一整张表的
GROUP BY底下仍然是一次Scan——Workbench 把那份成本摆到台面上,而不是把它藏起来,这与 Query 对比 Scan 是同一个权衡。 - 聚合是在行落地之后,对物化出来的标量属性运行的。
常见问题
我能不扫描就在 DynamoDB 里计数项吗?
不完全能。要一个精确的、当前的计数,你必须读取那些项——Select=COUNT 仍然计量每一个被计数的项。唯一的不扫描选项是近似的 DescribeTable.ItemCount(约每 6 小时更新一次),或者一个你自己在每次写入时维护的计数器项。
我怎么按一个 GSI 计数项?
对着索引用 Select=COUNT 运行 Query(或 Scan)。通过一个窄的 GSI 分区来计数,比扫描基表便宜得多,因为你只读取那个索引分区里的项——围绕你需要的计数来建模这个索引。
DescribeTable.ItemCount 准确吗?
它是近似的。API 参考 说明 DynamoDB“大约每六小时”更新 ItemCount 和 TableSizeBytes,而且“近期的改动可能不会反映在这个值里。”别在需要精确或实时数字的地方用它。
DynamoDB 能做 SUM 或 AVG 吗?
原生不能,PartiQL 里也不能——PartiQL SELECT 语法 没有聚合函数。在你的应用里聚合、维护一个计数器(可选地通过 DynamoDB Streams),或者在 DynoTable 的 SQL Workbench 里运行 SUM/AVG。
Count 和 ScannedCount 有什么区别?
ScannedCount 是 DynamoDB 在你的筛选之前求值了多少个项;Count 是筛选之后还剩多少个。当没有筛选表达式时它们相等。两者之间的大差距意味着一次低效的计数。
需要对你的 DynamoDB 数据求和、求平均或分组,又不想写一个扫描循环?下载 DynoTable,在一个 Workbench 标签页里运行它。想先比较各家客户端?看看它在 普通的 DynamoDB GUI 之中的位置。