DynamoDB Scan en Go (AWS SDK v2)

NewScanPaginator es un cursor, no una colección: empieza optimista, acaba agotado y toma su tamaño de página de un sitio que el ScanInput que tienes delante no muestra. Para saber cuándo evitar Scan por completo, mira Query vs. Scan.

Código

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))
}

Qué devuelve realmente HasMorePages()

Contra un conjunto de prueba de 600 canciones de unos 3,9 KB cada una, de las cuales 8 cumplen Year >= 2010 y quedan las últimas al ordenar:

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() es true antes de la primera llamada, así que el bucle siempre se ejecuta al menos una vez. No puedes usarlo para comprobar si una tabla está vacía. Luego dos páginas devolvieron cero Items mientras el scan seguía en marcha, lo que significa que if len(page.Items) == 0 { break } informa de un resultado vacío en una tabla que contiene 8 coincidencias. Y el paginador es de un solo uso: tras el bucle el valor está gastado, y NextPage devuelve un error en vez de reiniciarse. Volver a escanear significa volver a llamar a NewScanPaginator.

Limit vive en dos sitios, y gana la opción

Go reparte el tamaño de página entre la struct de entrada y las opciones propias del paginador. Pon ambos y la opción tiene prioridad:

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

Asignar in.Limit = aws.Int32(500) y esperar páginas de 500 Items es un fallo silencioso cuando una opción funcional de otro sitio ya puso 25. Si solo tocas ScanInput, el paginador lo respeta.

Medido el 2026-07-28 contra DynamoDB Local (amazon/dynamodb-local) con aws-sdk-go-v2/service/dynamodb v1.62.1 sobre go1.26.5.

Explicación

  • page.Items es []map[string]types.AttributeValue, no tu struct. attributevalue.UnmarshalListOfMaps(items, &songs) convierte el lote en una sola llamada y transforma Year en un int de Go; las etiquetas de struct dynamodbav controlan el mapeo.
  • Los números son cadenas en el cable. El valor en crudo es &types.AttributeValueMemberN{Value: "2010"} — el miembro N contiene un string de Go, así que strconv se sienta a ambos lados de cualquier aritmética salvo que hagas unmarshal a una struct tipada.
  • FilterExpression se ejecuta después de la lectura, y por eso las dos páginas vacías cuestan igualmente 128,5 unidades cada una. La referencia de la API es explícita en que filtrar "does not consume any additional read capacity units", y el corolario es que tampoco ahorra ninguna.
  • #filter0 es obligatorio, no cuestión de estilo. Year es una palabra reservada de DynamoDB; sin alias devuelve ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year.
  • Segment / TotalSegments dan a cada goroutine su propia porción de la tabla, y cada una necesita su propio paginador. El paralelismo recorta el tiempo de reloj y gasta la misma capacidad.

Hazlo visualmente

El constructor de consultas de DynamoDB emite la forma del programa entero — filtro, mapas de nombres y valores, y el bucle del paginador — para que las partes que mide esta página se generen en vez de recordarse.

Para ver cuántos Items toca de verdad un filtro antes de lanzar el scan contra un servicio, descarga DynoTable y ejecútalo contra la tabla en una cuadrícula.

Ejemplos relacionados

Referencias

Construye esta solicitud visualmente

Compón esta operación en el Generador de consultas de DynamoDB gratuito —condición de clave, filtro, índice, Limit, orden de clasificación y un bucle de paginación— y cópiala de vuelta como un programa ejecutable para SDK v3, CLI o boto3.

Abrir el Generador de consultas de DynamoDB

Trabaja con DynamoDB sin la Consola

Un cliente de escritorio rápido para DynamoDB que ejecuta el SQL real que DynamoDB no puede — JOINs, GROUP BY, agregaciones — con edición visual y un agente de IA con tus propias claves de Bedrock.

Prueba gratuita de 30 días, sin tarjeta — después, el plan Free sin límite de tiempo.