为什么 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),你为交回 40 个项,计量了大约 250,000 RCU——并翻阅了约 500 MB 的数据。
现在把这个访问模式建模为一个键,改为 Query 它:
Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"
这只读取一个分区中被匹配的那一片。如果那 40 行结账加上当天其他事件共约 2 MB,你为约 2 MB 的读取付费, 而不是 500 MB。同样的答案,成本的一个微小零头——而且延迟随表增长保持平稳。
Scan 与 Query,已计量
| Scan + 过滤 | 有键的 Query | |
|---|---|---|
| 读取 | 表中的每一个项 | 一个分区,由 SK 收窄 |
| 计费容量 | 整张表,在过滤之前 | 仅你那一片里的项 |
| 我们的例子 | 约 250,000 RCU(约 500 MB) | 几百个 RCU(约 2 MB) |
| 延迟 | 随表大小增长 | 随表增长保持平稳 |
| 结果数 | 对成本毫无决定作用 | 与你付的费相符 |
这张表编码的教训:在一个 Scan 上,你的结果数和你的账单无关。在一个 Query 上,它们彼此追随。
在你 Scan 之前先决定
大多数意外的 Scan 都来自一个问题:我能说出我需要的那个分区吗? 如果能,它就是一个 Query。
如果不能,修法是一个键,而不是一个更大的过滤。
这是流程图形式的那个决定。
这条路几乎总是终于 Query;只有当没有键——现有的或可添加的——契合这个访问模式时,你才落到 Scan。
如果这个模式真实且经常出现,但基础表无法给它设键,那就是加一个 全局二级索引
的信号,让这个问题变成一次 Query。提前围绕你的访问模式建模你的键,就是全部的游戏——参见
单表设计。
写有键的查询,而不是一个过滤
当你确实需要一个超出键的条件时,刻意地构建它,而不是把一切都倒进一个 FilterExpression。
DynamoDB Expression Builder 替你生成
KeyConditionExpression 和属性占位符,于是分区键和排序键做收窄——在 DynamoDB 计量读取之前,而不是之后。
KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)
何时一个 Scan 其实没问题
一个 Scan 并非被禁止——它只是错误的默认。当你真的意指"读取一切"时,它是正确的工具:
- 一次性导出或手工运行的回填。
- 微小的配置/查找表,整张表才几 KB。
- 后台作业,故意翻遍整张表。用
Segment/TotalSegments把它们拆给多个工作者——一次并行扫描 ——而不是一次漫长的顺序爬行。(AWS: 扫描表)
并且注意 PartiQL 救不了你:SELECT * FROM ProductEvents WHERE eventType = 'checkout' 没有键谓词,
直接编译成一个 Scan。这是穿着 SQL 外衣的同一个坑。(参见 Query 与 Scan
了解完整拆解。)
当你真正需要跨项分析——一个 GROUP BY、一个 JOIN、一个 DynamoDB 表达不了的聚合——DynoTable 的
SQL Workbench 在一个有界的结果集上客户端地运行它们,而不是用一次完整 Scan 去捶打表。
下一步
用容量计算器估算任一模式的成本,读 Query 与 Scan了解 API 层面的对比,并 下载 DynoTable 对你自己的表运行这些,亲眼看消耗的容量。