入门阅读约 3 分钟

为什么 DynamoDB Scan 又慢又贵

一次 Scan 会读取表中的每一个项,之后才做过滤。它是 你出于 SQL 肌肉记忆而伸手去用的操作,也是那个悄悄 推高你账单、同时把延迟拖得比你逃离的 RDS 机器还差的操作。

为什么我的 DynamoDB Scan 又慢又贵?

一次 Scan 会在 FilterExpression 运行之前读取表中的每一个项,所以 无论最终返回多少行,你都要为读取整张表付费,而且它会 随着表的增长而变慢。修复之道几乎总是一次按键的 Query —— 围绕某个键 对访问模式建模,让 DynamoDB 只触及一个分区,而不是 全部。

  • Scan 每次都读取整张表。 决定你付多少费、耗多长时间的是表的 大小,而不是你的结果条数。
  • FilterExpression 是关于成本的一个谎言。 它在读取被计量_之后_ 才运行,所以返回 12 个项也可能按读取 1200 万个来计费。
  • Scan 会随着你的增长而变慢。 而一次按键的 Query 保持平稳 —— 无论表变得 多大,它都只触及一个分区。
  • 修复之道几乎总是建模,而不是调优。 如果你靠 Scan 来回答一个 日常问题,那说明你少了一个键。

Scan 实际上做了什么

从 SQL 过来,SELECT * FROM events WHERE type = 'checkout' 感觉是免费的 —— 引擎要么有索引,要么没有,但无论哪种情况你都能拿回行。在 DynamoDB 里,没有一个查询规划器替你做那个决定。

一次 Scan 会顺序走遍整张表,每次 1 MB,把每 一页交给你的 FilterExpression。凡是被过滤器拒绝的都仍然被读取、 仍然被计量、仍然算在你的账单上。(AWS:扫描表

这就是陷阱所在。过滤器看起来像一个 WHERE 子句,但它改变的是 结果集,绝不改变成本。无论 是否存在过滤器,Scan 都消耗相同的读取容量。(AWS:扫描表

数一数读取单元

DynamoDB 以(RCU)来计量读取。一个 RCU 可以买到对 一个最大 4 KB 的项做一次读取;读取的 成本是它的一半。更大的项向上取整到下一个 4 KB。(AWS:读/写 容量模式

设想一张分析表 ProductEvents。每一行是一个被追踪的事件:

PK  = "TENANT#acme"
SK  = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytes

假设它存了 2,000,000 个事件,每个约 1 KB,全都归在一个繁忙的租户下。你 想要今天的结账事件。条件反射式的做法:

Scan ProductEvents
FilterExpression: eventType = "checkout"

那个过滤器也许返回 40 行。但 Scan 先读取了全部 2,000,000 个 项。按每个约 1 KB 计(每 4 KB 1 RCU,最终一致约为每 4 KB 0.5 RCU), 你计量了大约 250,000 RCU —— 并翻遍了约 2 GB 的数据 —— 才 交回 40 个项。

现在把访问模式建模成一个键,改用 Query

Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"

这只读取一个分区中匹配的那一小片。如果那 40 行结账事件 加上当天的其他事件合计约 2 MB,你为约 2 MB 的读取付费,而不是 2 GB。同样的答案,成本却只是一个零头 —— 而且延迟会 随着表的增长保持平稳。

Scan 与 Query,计量对比

Scan + 过滤器按键 Query
读取表中的每一个一个分区,由 SK 收窄
计费容量整张表,在过滤之前只有你那一小片里的项
我们的示例约 250,000 RCU(约 2 GB)几百个 RCU(约 2 MB)
延迟随表大小增长随表增长保持平稳
结果条数对成本毫无影响与你的付费相匹配

Scan 上,你的结果条数和你的账单 毫无关系。而在 Query 上,二者彼此对应。

在 Scan 之前先做判断

大多数意外的 Scan 都源自一个问题:我能说出我需要的那个分区吗? 如果能,那就是一次 Query。如果不能,修复之道是加一个键,而不是加一个更大的过滤器。

下面把这个判断画成流程图。

需要读取项知道分区键吗?Query —— 一个分区能用 GSI 给它做键吗?加一个 GSI,然后 QueryScan —— 最后手段

这条路径几乎总是终结于 Query;只有当没有键 —— 无论是现成的 还是可添加的 —— 适配该访问模式时,你才会跌落到 Scan

如果这个模式是真实且反复出现的,但基表无法为它建键,那就是 加一个全局二级索引的信号,好让这个问题 变成一次 Query。事先围绕你的访问模式来建模你的键, 才是整场博弈的全部 —— 参见单表设计

写出按键的查询,而不是过滤器

当你确实需要一个键以外的条件时,请刻意地把它构建出来,而不是 把一切都倾倒进一个 FilterExpressionDynamoDB 表达式构建器会为你生成 KeyConditionExpression 和属性占位符,让分区键 和排序键来做收窄 —— 在 DynamoDB 计量读取_之前_,而不是之后。

KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)

Scan 什么时候其实没问题

Scan 并非被禁止 —— 它只是错误的默认选项。当你真心是想 "读取全部"时,它才是对的工具:

  • 手动运行的一次性导出或回填。
  • 微小的配置/查找表,整张表也就几 KB。
  • 有意翻遍整张表的后台作业。用 Segment / TotalSegments 把它们 拆分到多个 worker —— 一次 —— 而不是 一次漫长的顺序爬取。(AWS:扫描表

另外请注意 PartiQL 救不了你:SELECT * FROM ProductEvents WHERE eventType = 'checkout' 若没有键谓词,就会直接编译成一次 Scan。 这是同一个陷阱,只是穿了 SQL 的外衣。(完整的拆解参见 Query 与 Scan 对比。)

当你真的需要跨项分析 —— 一次 GROUP BY、一次 JOIN、一个 DynamoDB 无法表达的聚合 —— DynoTable 的 SQL Workbench 会在有界的结果集 上于客户端运行它们,而不是用一次全表 Scan 去猛捶那张表。

后续步骤

定价计算器估算这两种模式各自的 成本,阅读 Query 与 Scan 对比了解 API 层面的 差异,然后下载 DynoTable,针对你自己的表运行这些操作,看清 每种做法实际读取了多少个项。

更新于