Java(AWS SDK v2)中的 DynamoDB Scan
scanPaginator 会替你处理 LastEvaluatedKey,而大多数人接下来会去拧的那个旋钮——.limit(...)——反而让同一次扫描更慢、还略微更贵。至于什么时候干脆别用 Scan,见 Query 与 Scan 的取舍。
代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
public class ScanExample {
public static void main(String[] args) {
try (DynamoDbClient ddb = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build()) {
Map<String, String> names = new HashMap<>();
names.put("#filter0", "Year");
Map<String, AttributeValue> values = new HashMap<>();
values.put(":filterValue0", AttributeValue.builder().n("2010").build());
ScanRequest request = ScanRequest.builder()
.tableName("Music")
.filterExpression("#filter0 >= :filterValue0")
.expressionAttributeNames(names)
.expressionAttributeValues(values)
.build();
List<Map<String, AttributeValue>> items = new ArrayList<>();
for (ScanResponse page : ddb.scanPaginator(request)) {
items.addAll(page.items());
}
System.out.println("Matched " + items.size() + " items");
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
}
}
}.limit(25) 把 3 次请求变成 25 次,而且更贵
样本数据里有 600 首歌,每首大约 3.9 KB,其中 8 首命中。先按原样跑一遍示例,再加上 .limit(25) 跑一遍:
| 请求形态 | 往返次数 | 读单元 | 返回的项数 |
|---|---|---|---|
| 原样 | 3 | 284.5 | 8 |
.limit(25) | 25 | 288.0 | 8 |
容量是按页计费的,并且向上取整到 4 KB 边界,所以把一次 1 MB 的读取切成 24 次小读取,就把那个取整付了 24 遍。第 25 次请求是另一个意外:第 24 页已经读完了整张表,却仍然返回了一个 LastEvaluatedKey,于是分页器又问了一次,拿到 scannedCount=0。DynamoDB 表示"没有更多数据"的方式是不返回那个键,而不是返回一个短页;而在 Limit 边界上,它还不知道。
.limit(...) 是一个容量平滑旋钮,用在你不希望它把线上表打限流的后台任务上。它不是让扫描更便宜或更短的办法。
别名不是风格问题
去掉 #filter0、直接在 Year 上过滤,SDK 会通过 awsErrorDetails() 把这个抛出来:
DynamoDbException / ValidationException /
Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year
/ http 400Year 是 573 个保留字之一。和这个 SDK 里每一个分页器错误一样,它是在 for 循环的第一次迭代时到达的,而不是在你调用 scanPaginator(request) 时,所以 try 必须把循环包起来。
2026-07-28 针对 DynamoDB Local(amazon/dynamodb-local)、在 OpenJDK 26.0.1 上用 software.amazon.awssdk:dynamodb 2.49.4 测量。
说明
- 前两页返回零个项。用这个过滤器时,每页的结果分别是 0、0 和 8,对应 128.5、128.5 和 27.5 个读单元。
filterExpression是在读取之后才跑的,所以那两个空响应是全价的,而任何if (page.items().isEmpty()) break都会报告成空表。 ddb.scanPaginator(request).items()会把各页压平成单个Iterable<Map<String, AttributeValue>>并在你身后翻页,当你只想要项时,它能把嵌套循环收掉。它是一个SdkIterable,所以.stream()可用——这里.items().stream().count()返回 8。ScanIterable每次迭代都会重新跑一遍扫描。它是惰性的,不是缓存的:对同一个对象循环两次就会发两遍请求、计两遍费。像示例那样,一次性把它抽干到一个List里。- 构建器里的数字是
String。AttributeValue.builder().n("2010")接收的是java.lang.String,因为 DynamoDB 是以十进制文本传输数字的。传int编译不过。 .segment(...)/.totalSegments(...)会把一次全表扫描拆给多个 worker,各自带一个分页器。这能把墙上时钟的时间除以几份,但花掉的容量是一样的。
用可视化的方式来做
保留字检查器会拿完整的 AWS 列表跑一遍你的属性名,并把 ExpressionAttributeNames 映射交给你,这比一次一个 ValidationException 地弄明白 Year、Name、Size 和 Status 都被占用了要快得多。
要在把过滤器接进 ScanRequest 之前先在真实的表上试一试,请下载 DynoTable,然后在表格里翻结果。
相关示例
- Go 中的 DynamoDB Scan——用 AWS SDK for Go v2 实现同一次扫描。
- Java 中的 DynamoDB Query——通常你该优先选的那种更便宜的读取。
- Query 与 Scan 的取舍——什么时候(很少)一次
Scan是合理的。 - 为什么我的 DynamoDB Scan 又慢又贵?——成本模型,以及怎么避开它。
- DynamoDB ProvisionedThroughputExceededException——一次全表扫描会把一张预置表的容量弄成什么样。
- DynamoDB ThrottlingException——另一种限流,以及指数退避是怎么应对它的。
参考资料
- Scan — Amazon DynamoDB API Reference
- Use Scan with an AWS SDK or CLI — Amazon DynamoDB Developer Guide
- DynamoDbClient — AWS SDK for Java 2.x API Reference
- ScanRequest — AWS SDK for Java 2.x API Reference
- Scanning tables — Amazon DynamoDB Developer Guide
- Reserved words in DynamoDB — Amazon DynamoDB Developer Guide