DynamoDB Scan en Go (AWS SDK v2)

NewScanPaginator est un curseur, pas une collection : il démarre optimiste, finit épuisé, et prend sa taille de page à un endroit que le ScanInput sous tes yeux ne montre pas. Pour savoir quand éviter Scan complètement, voir Query vs. Scan.

Code

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func main() {
	ctx := context.TODO()
	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
	if err != nil {
		log.Fatalf("load config: %v", err)
	}
	client := dynamodb.NewFromConfig(cfg)

	paginator := dynamodb.NewScanPaginator(client, &dynamodb.ScanInput{
		TableName:        aws.String("Music"),
		FilterExpression: aws.String("#filter0 >= :filterValue0"),
		ExpressionAttributeNames: map[string]string{
			"#filter0": "Year",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":filterValue0": &types.AttributeValueMemberN{Value: "2010"},
		},
	})

	var items []map[string]types.AttributeValue
	for paginator.HasMorePages() {
		page, err := paginator.NextPage(ctx)
		if err != nil {
			log.Fatalf("scan: %v", err)
		}
		items = append(items, page.Items...)
	}
	fmt.Printf("Matched %d items\n", len(items))
}

Ce que HasMorePages() renvoie réellement

Sur un jeu d'essai de 600 morceaux d'environ 3,9 Ko chacun, dont 8 correspondent à Year >= 2010 et se trient en dernier :

fresh HasMorePages():                    true    (before any request)
page 1: len(page.Items)=0  ScannedCount=271  CU=128.5  LastEvaluatedKey set
page 2: len(page.Items)=0  ScannedCount=271  CU=128.5  LastEvaluatedKey set
page 3: len(page.Items)=8  ScannedCount= 58  CU= 27.5  LastEvaluatedKey nil
exhausted HasMorePages():                false
NextPage() after exhaustion:             err = "no more pages available"

HasMorePages() vaut true avant le premier appel, donc la boucle se déclenche toujours au moins une fois. Tu ne peux pas t'en servir pour tester si une table est vide. Deux pages ont ensuite renvoyé zéro élément alors que le scan tournait encore, ce qui veut dire que if len(page.Items) == 0 { break } signale un résultat vide sur une table qui contient 8 correspondances. Et le paginateur est à usage unique : après la boucle la valeur est consommée, et NextPage renvoie une erreur au lieu de repartir. Rescanner, c'est rappeler NewScanPaginator.

Limit vit à deux endroits, et c'est l'option qui gagne

Go répartit la taille de page entre la struct d'entrée et les options propres au paginateur. Règle les deux et l'option l'emporte :

ScanInput.Limit = 500, ScanPaginatorOptions.Limit = 25  ->  ScannedCount 25

Affecter in.Limit = aws.Int32(500) en attendant des pages de 500 éléments passe inaperçu quand une option fonctionnelle ailleurs a déjà mis 25. Si tu ne touches qu'au ScanInput, le paginateur le respecte.

Mesuré le 2026-07-28 contre DynamoDB Local (amazon/dynamodb-local) avec aws-sdk-go-v2/service/dynamodb v1.62.1 sur go1.26.5.

Explication

  • page.Items est un []map[string]types.AttributeValue, pas ta struct. attributevalue.UnmarshalListOfMaps(items, &songs) convertit le lot en un seul appel et transforme Year en int Go ; les tags de struct dynamodbav pilotent le mapping.
  • Les nombres sont des chaînes sur le fil. La valeur brute est &types.AttributeValueMemberN{Value: "2010"} — le membre N contient une string Go, donc strconv se retrouve des deux côtés de toute arithmétique, sauf si tu démarshalles dans une struct typée.
  • FilterExpression s'exécute après la lecture, ce qui explique pourquoi les deux pages vides coûtent quand même 128,5 unités chacune. La référence de l'API est explicite : le filtrage "does not consume any additional read capacity units", et le corollaire est qu'il n'en économise aucune non plus.
  • #filter0 est obligatoire, pas stylistique. Year est un mot réservé DynamoDB ; sans alias, il renvoie ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year.
  • Segment / TotalSegments donnent à chaque goroutine sa propre tranche de la table, et chacune a besoin de son propre paginateur. Le parallélisme réduit le temps d'exécution et dépense la même capacité.

Le faire visuellement

Le constructeur de requêtes DynamoDB émet la forme complète du programme — filtre, maps de noms et de valeurs, et la boucle du paginateur — pour que les éléments mesurés par cette page soient générés plutôt que mémorisés.

Pour voir combien d'éléments un filtre touche réellement avant de lancer le scan sur un service, télécharge DynoTable et exécute-le sur la table 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.