Scan do DynamoDB em Java (AWS SDK v2)
O scanPaginator cuida do LastEvaluatedKey por você, e o botão que a maioria das pessoas gira em seguida — .limit(...) — deixa o mesmo scan mais lento e um pouco mais caro. Para saber quando evitar o Scan por completo, veja Query vs. Scan.
Código
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) transforma 3 requisições em 25 e custa mais
O conjunto de teste tem 600 músicas de aproximadamente 3,9 KB cada, e 8 delas correspondem ao filtro. Rode o exemplo como está escrito, depois rode de novo com .limit(25):
| Formato da requisição | Idas e voltas | Unidades de leitura | Itens retornados |
|---|---|---|---|
| como está escrito | 3 | 284,5 | 8 |
.limit(25) | 25 | 288,0 | 8 |
A capacidade é cobrada por página, arredondada para cima em um limite de 4 KB, então fatiar uma leitura de 1 MB em 24 pequenas paga o arredondamento 24 vezes. A 25ª requisição é a outra surpresa: a página 24 terminou a tabela e ainda assim retornou um LastEvaluatedKey, então o paginador perguntou mais uma vez e recebeu scannedCount=0. O DynamoDB sinaliza "não há mais dados" omitindo essa chave, não retornando uma página curta, e em um limite de Limit ele ainda não sabe.
.limit(...) é um botão de suavização de capacidade para um job em segundo plano que você não quer que faça throttle em uma tabela ao vivo. Não é um jeito de tornar um scan mais barato ou mais curto.
O alias não é estilístico
Remova #filter0 e filtre por Year diretamente, e o SDK expõe isto através de awsErrorDetails():
DynamoDbException / ValidationException /
Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year
/ http 400Year é uma das 573 palavras reservadas. Como todo erro de paginador neste SDK, ele chega na primeira iteração do laço for, não quando você chama scanPaginator(request), então o try precisa envolver o laço.
Medido em 2026-07-28 contra o DynamoDB Local (amazon/dynamodb-local) com software.amazon.awssdk:dynamodb 2.49.4 no OpenJDK 26.0.1.
Explicação
- As duas primeiras páginas retornam zero itens. Com este filtro os resultados por página são 0, 0 e 8, a 128,5, 128,5 e 27,5 unidades de leitura. A
filterExpressionroda depois da leitura, então essas duas respostas vazias custam preço cheio, e qualquerif (page.items().isEmpty()) breakreporta uma tabela vazia. ddb.scanPaginator(request).items()achata as páginas em um únicoIterable<Map<String, AttributeValue>>e pagina nos bastidores, o que colapsa o laço aninhado quando você só quer os itens. É umSdkIterable, então.stream()funciona —.items().stream().count()retorna 8 aqui.- O
ScanIterablere-executa o scan a cada iteração. Ele é preguiçoso, não cacheado: iterar sobre o mesmo objeto duas vezes envia as requisições duas vezes e cobra duas vezes. Esvazie-o em umaListuma única vez, como o exemplo faz. - Números são
Stringno builder.AttributeValue.builder().n("2010")recebe umajava.lang.String, porque o DynamoDB transporta números como texto decimal. Passar umintnão compila. .segment(...)/.totalSegments(...)dividem um scan de tabela inteira entre workers, cada um com seu próprio paginador. Isso divide o tempo de relógio e gasta a mesma capacidade.
Faça isso visualmente
O verificador de palavras reservadas roda os nomes dos seus atributos contra a lista completa da AWS e devolve o mapa ExpressionAttributeNames, o que é mais rápido do que descobrir que Year, Name, Size e Status estão todos ocupados um ValidationException de cada vez.
Para testar um filtro contra uma tabela real antes de encaixá-lo em um ScanRequest, baixe o DynoTable e navegue pelos resultados em uma grade.
Exemplos relacionados
- Scan do DynamoDB em Go — o mesmo scan com o AWS SDK for Go v2.
- Query do DynamoDB em Java — a leitura mais barata que você normalmente deveria escolher.
- Query vs. Scan — quando (raramente) um
Scanse justifica. - Por que meu Scan do DynamoDB é lento e caro? — o modelo de custo e como evitá-lo.
- DynamoDB ProvisionedThroughputExceededException — o que um scan de tabela inteira faz com a capacidade de uma tabela provisionada.
- DynamoDB ThrottlingException — o outro throttle, e como o backoff exponencial lida com ele.
Referências
- 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