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) 跑一遍:

请求形态往返次数读单元返回的项数
原样3284.58
.limit(25)25288.08

容量是按页计费的,并且向上取整到 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 400

Year 是 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 里。
  • 构建器里的数字是 StringAttributeValue.builder().n("2010") 接收的是 java.lang.String,因为 DynamoDB 是以十进制文本传输数字的。传 int 编译不过。
  • .segment(...) / .totalSegments(...) 会把一次全表扫描拆给多个 worker,各自带一个分页器。这能把墙上时钟的时间除以几份,但花掉的容量是一样的。

用可视化的方式来做

保留字检查器会拿完整的 AWS 列表跑一遍你的属性名,并把 ExpressionAttributeNames 映射交给你,这比一次一个 ValidationException 地弄明白 YearNameSizeStatus 都被占用了要快得多。

要在把过滤器接进 ScanRequest 之前先在真实的表上试一试,请下载 DynoTable,然后在表格里翻结果。

相关示例

参考资料

可视化构建此请求

在免费的 DynamoDB 查询构建器中组装此操作 —— 键条件、筛选、索引、Limit、排序方向和分页循环 —— 再把它作为可运行的 SDK v3、CLI 或 boto3 程序复制回来。

打开 DynamoDB 查询构建器

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。