DynamoDB GROUP BY:如何在没有 GROUP BY 子句的情况下聚合
DynamoDB 中没有 GROUP BY。也没有 COUNT、SUM 或 AVG——
原生 API 里没有,PartiQL 里也没有。DynamoDB 是键值 /
文档存储,不是分析引擎,所以聚合是你要
构建的东西,而不是查询规划器为你做的事。
能在 DynamoDB 中使用 GROUP BY 吗?
不能。DynamoDB 没有 GROUP BY、HAVING,也没有 COUNT、SUM、AVG 这类聚合函数——原生 API 里没有,PartiQL 里也没有,其 SELECT 只接受 WHERE 和 ORDER BY。你需要在数据变化时预先计算总数来实现聚合(原子计数器,或 Streams + Lambda 汇总),或者在读取之后在应用侧分组。
- DynamoDB 的 PartiQL
SELECT语法是SELECT … FROM … [WHERE …] [ORDER BY …]——这就是全部列表。没有GROUP BY、没有HAVING、没有聚合 函数、没有JOIN(AWS PartiQLSELECT参考)。 - 因为 DynamoDB "不原生支持跨项的
SUM或COUNT等聚合 操作",AWS 自己的指导是在数据变化时预先计算聚合, 并把结果存为普通的项 (AWS:物化聚合)。 - 另一种做法——读取每个项再在你的应用中聚合——可行,但你 在每次查询时都要付费读取整张表。
- 对于一次性探索,DynoTable 的 SQL Workbench 能直接对实时表运行
GROUP BY/COUNT/SUM/AVG——也就是 DynamoDB 的 PartiQL 端点拒绝的那种 SQL。
为什么聚合在 DynamoDB 中很难
DynamoDB 没有扫描时聚合引擎。Query 和 Scan 返回项;
它们不折叠项。Scan 每次以 1 MB 为单位读取整张表,且
它消耗的容量是基于它读取的项,而非你保留的行——
FilterExpression 是在扫描之后、结果返回之前应用的,所以它
在缩小结果集的同时不降低费用(AWS Scan API 参考:过滤器
"不消耗任何额外的读容量单位";容量是基于扫描的项
大小,而非返回的)。一开始就没有 GROUP-BY 钩子
来挂上求和或计数。
PartiQL 没有改变这一点。PartiQL 是基于同一引擎的 SQL兼容
方言,所以它继承了同样的限制——它是一个语法接口,而非新的
执行模型。记载的 SELECT 语法
根本没有 GROUP BY 关键字。
关于 PartiQL 与真正 SQL 之间的完整差距,参见
PartiQL vs SQL。
所以问题不是"我该怎么写一个 GROUP BY"——而是"我的
聚合存在哪里,何时计算?"有三个答案。
模式 1:写入时聚合(原子计数器)
如果你提前知道分组——按状态计数、按客户求和、 按月统计下载量——就保留一个计数器项,并在每次写入时更新它。
使用 ADD 更新表达式,让递增是原子且并发安全的。
ADD 作用于数字和集合,并且它避免了读-改-写竞态,所以
两个写入者递增同一个计数器永远不会互相覆盖
(AWS 指出原子的 ADD "避免了读-改-写竞态条件"):
UpdateItem
Key { pk: "STATS#orders", sk: "status#shipped" }
UpdateExpression "ADD orderCount :one"
ExpressionAttributeValues { ":one": 1 }
这就是你的 SELECT COUNT(*) … GROUP BY status——只不过计数已经
作为一个项就在那里,可在一次个位数毫秒的 GetItem 中读取。
取舍是:你必须在写入时知道分组键,并且你把计数器
更新耦合到了写入路径上。如果应用在写入之后、
计数器更新之前崩溃,两者会失去同步——而这正是
下一个模式所解耦的失败模式。
模式 2:DynamoDB Streams + Lambda 汇总
当你不想把聚合逻辑放在写入路径上——或写入是
一个你无法轻易包裹的普通 PutItem 时——把它下移。这是 AWS 自己
推荐的模式,物化聚合
(AWS:使用 GSI 进行物化聚合查询):
- 应用写入原始项(一个订单、一次下载、一个事件)。没有聚合 逻辑。
- DynamoDB Streams 把这次写入捕获为一条流记录。
- 一个挂在流上的 Lambda 读取新项,推导出分组
(状态、月份、类别……),并用一次原子
UpdateItem对匹配的聚合项ADD——当许多次调用触及同一个计数器时,它"避免了读-改-写竞态条件"。 - 你查询预计算的聚合——常常通过一个稀疏 GSI,它
只索引汇总项,于是"本月前 10"就是一次带
Limit 10的Query。
稀疏 GSI 技巧:只有聚合项携带被索引的属性
(例如 Month),所以原始事件行被自动排除在索引之外
——"占表中总项数的一小部分",这让索引保持
廉价、读取保持快速。
这把聚合与写入路径解耦,并让写入保持简单,代价是 最终一致——AWS 指出"从记录一次下载到聚合被更新之间 有几秒钟的延迟"。对于仪表盘、 排行榜和趋势计数器来说这没问题。
同样的重试警告适用:一次被重试的 Lambda 调用会重新运行 ADD,所以
"重试会使计数多增不止一次",留下一个近似
值。要得到精确计数,加上幂等性(例如一个以
源项 id 为键的条件表达式);否则对于分析和
排行榜,小幅误差是可以接受的。
模式 3:在 Scan/Query 之后的应用侧分组
最暴力的选项:读取项,在你的代码中分组它们。
groups = {}
for item in paginate(table.scan()): # 或对单个分区用 query()
key = item["status"]
groups[key] = groups.get(key, 0) + 1这是正确的,有时也是正确的选择——但要诚实面对成本。一次
Scan 会读取表中的每个项,无论你是否过滤,读容量
都相同。所以对整次 Scan 做应用侧分组意味着你在
每次聚合时都要付费读取整张表,且延迟随表增长。
AWS 把"读取时扫描并计数"列为"仅适用于延迟不敏感的
极小数据集"
(AWS:为何预计算聚合)。
通过 Query 缩小到单个分区(例如统计一个
客户的订单),应用侧分组完全合理——你只读取一个
项集合。关于两者之间的完整成本差距,参见
Query vs Scan。要在运行前估算某次聚合
扫描将读取多少,用
项大小计算器为一个有代表性的项估算大小——读容量
按每 4 KB 向上取整,
所以项大小驱动费用。
对于真正临时的、对 DynamoDB 表的分析型 SQL——你只跑一次的那种
"GROUP BY status,把它们计数"——AWS 的答案是用
一个独立引擎对准它:Amazon Athena DynamoDB 连接器让你
用真正的 SQL(GROUP BY、聚合,甚至与其他源的 JOIN)查询
表,通过一个 Lambda 连接器
(AWS:Amazon Athena DynamoDB 连接器)。
它在幕后扫描表,所以它是报表/BI 工具,而非热点路径。
我该用哪种模式?
| 你需要…… | 使用 |
|---|---|
| 热点读取路径上一个已知分组的总数 | 模式 1——原子计数器(ADD) |
| 不触碰写入路径的聚合 | 模式 2——Streams + Lambda 汇总 |
| 缩小到单个分区的计数 | 模式 3——Query 后在应用中分组 |
| 精确总数,无漂移 | 模式 1/2 加上幂等性防护 |
探索时的一次性 GROUP BY | DynoTable Workbench(见下)或 Athena |
| 用 SQL 做周期性 BI/报表 | Athena DynamoDB 连接器 |
在 DynoTable 的 SQL Workbench 中直接运行 GROUP BY
上面的模式是你在生产中服务聚合的方式。但当你 在探索一张表时——"现在每个状态有多少订单?"——你不想 去预置 Lambda 或搭建 Athena。你想直接键入查询。
这正是 DynoTable 的 SQL Workbench 的用途。它运行真正的 SQL——
GROUP BY、COUNT、SUM、AVG、HAVING,甚至 JOIN——直接对你的
实时 DynamoDB 表执行,在它读取的行上以客户端方式执行聚合。它就是
DynamoDB 的 PartiQL 端点拒绝的那种 SQL:
SELECT status, COUNT(*) AS orders, SUM(total) AS revenue
FROM "Orders"
GROUP BY status
HAVING SUM(total) > 1000
ORDER BY revenue DESC诚实的说法:在底层 DynoTable 以 API 允许的方式读取项
(能用 Query 的地方用 Query,必须用 Scan 的地方用 Scan),把它们物化,
然后在 Workbench 中做分组——与模式 3 相同的"读取再聚合"
机制,只是没有那个循环,并且在 DynamoDB 的访问模式规则内。它
是为探索和临时分析而构建的,不是为了在热点读取路径上替换生产
汇总。要做那件事,请预计算(模式 1 / 2)。
关于同一切入点的 JOIN 一面——DynoTable 也能运行 PartiQL 做不到的
跨表连接——参见 DynamoDB JOIN。要在
这一能力上比较各 GUI 客户端?参见
DynamoDB GUI 对比。
常见问题
DynamoDB PartiQL 支持 GROUP BY 吗?
不支持。DynamoDB 的 PartiQL SELECT 仅支持 WHERE 和 ORDER BY——没有
GROUP BY、HAVING、聚合函数或 JOIN。该语法
记载为
SELECT … FROM … [WHERE …] [ORDER BY …]。
我能对整张 DynamoDB 表做 COUNT(*) 吗?
作为聚合函数不能——PartiQL 没有聚合函数。该 API 给你
在 Scan/Query 上的 Select=COUNT,它返回匹配项的计数,但
仍会读取(并计费)扫描触及的每个项
(AWS Scan API 参考:
容量基于检查的项,而非返回的)。对于频繁读取的
总数,保留一个计数器项(模式 1)。
我能按分区键 GROUP BY 吗?
在 DynamoDB 或 PartiQL 中不能。如果"按分区键"是一个已知的访问模式,
就用原子 ADD 为每个键维护一个聚合项(模式 1),或用
Streams + Lambda 汇总它(模式 2)。
如何按组做 SUM 或 AVG?
SUM:为每个组保留一个累计总数,并在写入时对它 ADD。AVG:同时存储
求和与计数,在读取时相除——没有原生的平均值。
对于一次性探索的 AVG,在 DynoTable 的 SQL Workbench 或通过
Athena DynamoDB 连接器运行它。
有没有 partiql group by 的变通方法?
没有 PartiQL 侧的变通方法。要么预计算聚合(计数器/Streams)并
SELECT 那个汇总项,要么在一个有 GROUP BY 的引擎中运行它——
临时用 DynoTable 的 Workbench,周期性报表用 Athena。
想对你自己的表运行 GROUP BY 而不写 Lambda?
试用 DynoTable,把 SQL Workbench 指向一张实时表。