Scan DynamoDB en Java (AWS SDK v2)

scanPaginator gère LastEvaluatedKey à ta place, et le seul bouton que la plupart des gens actionnent ensuite — .limit(...) — rend le même scan plus lent et légèrement plus cher. Pour savoir quand éviter Scan complètement, voir Query vs Scan.

Code

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) transforme 3 requêtes en 25 et coûte plus cher

Le jeu de test contient 600 morceaux d'environ 3,9 Ko chacun, dont 8 correspondent. Lance l'exemple tel quel, puis relance-le avec .limit(25) :

Forme de la requêteAllers-retoursUnités de lectureÉléments renvoyés
tel quel3284,58
.limit(25)25288,08

La capacité est facturée par page, arrondie à la frontière des 4 Ko : découper une lecture de 1 Mo en 24 petites paie donc l'arrondi 24 fois. La 25e requête est l'autre surprise : la page 24 a terminé la table et a quand même renvoyé un LastEvaluatedKey, donc le paginateur a redemandé une fois de plus et a reçu scannedCount=0. DynamoDB signale « plus de données » en omettant cette clé, pas en renvoyant une page courte, et à une frontière de Limit il ne le sait pas encore.

.limit(...) est un bouton de lissage de capacité pour un job d'arrière-plan dont tu ne veux pas qu'il throttle une table en production. Ce n'est pas un moyen de rendre un scan moins cher ou plus court.

L'alias n'est pas stylistique

Supprime #filter0 et filtre directement sur Year : le SDK fait remonter ceci via awsErrorDetails() :

DynamoDbException / ValidationException /
Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year
/ http 400

Year fait partie des 573 mots réservés. Comme toute erreur de paginateur dans ce SDK, elle arrive à la première itération de la boucle for, et non à l'appel de scanPaginator(request) : le try doit donc envelopper la boucle.

Mesuré le 2026-07-28 sur DynamoDB Local (amazon/dynamodb-local) avec software.amazon.awssdk:dynamodb 2.49.4 sur OpenJDK 26.0.1.

Explication

  • Les deux premières pages ne renvoient aucun élément. Avec ce filtre, les résultats par page sont 0, 0 puis 8, à 128,5, 128,5 et 27,5 unités de lecture. filterExpression s'exécute après la lecture, donc ces deux réponses vides coûtent plein tarif, et tout if (page.items().isEmpty()) break rapporte une table vide.
  • ddb.scanPaginator(request).items() aplatit les pages en un unique Iterable<Map<String, AttributeValue>> et pagine derrière toi, ce qui supprime la boucle imbriquée quand tu ne veux que les éléments. C'est un SdkIterable, donc .stream() fonctionne — .items().stream().count() renvoie 8 ici.
  • ScanIterable relance le scan à chaque itération. Il est paresseux, pas mis en cache : boucler deux fois sur le même objet envoie les requêtes deux fois et facture deux fois. Vide-le une bonne fois dans une List, comme le fait l'exemple.
  • Les nombres sont des String dans le builder. AttributeValue.builder().n("2010") prend un java.lang.String, parce que DynamoDB transporte les nombres sous forme de texte décimal. Passer un int ne compilera pas.
  • .segment(...) / .totalSegments(...) répartissent un scan de table complète entre plusieurs workers, chacun avec son propre paginateur. Ça divise le temps écoulé et dépense la même capacité.

Le faire visuellement

Le vérificateur de mots réservés confronte tes noms d'attributs à la liste complète d'AWS et te rend la map ExpressionAttributeNames, ce qui est plus rapide que d'apprendre que Year, Name, Size et Status sont tous pris, une ValidationException à la fois.

Pour essayer un filtre sur une vraie table avant de le câbler dans un ScanRequest, télécharge DynoTable et parcours les résultats dans une grille.

Exemples liés

Références

Construis cette requête visuellement

Compose cette opération dans le Générateur de requêtes DynamoDB gratuit — condition de clé, filtre, index, Limit, ordre de tri et boucle de pagination — et copie-la en retour comme programme exécutable SDK v3, CLI ou boto3.

Ouvrir le Générateur de requêtes DynamoDB

Travaille avec DynamoDB sans la Console

Un client de bureau rapide pour DynamoDB qui exécute le vrai SQL que DynamoDB ne peut pas — JOINs, GROUP BY, agrégations — avec édition visuelle et un agent IA sur tes propres clés Bedrock.

Essai gratuit de 30 jours, sans carte bancaire — ensuite la formule Gratuit, sans limite de durée.