Go での DynamoDB Scan(AWS SDK v2)

NewScanPaginator はコレクションではなくカーソルです。楽観的に始まり、使い切って終わり、そしてページサイズを、目の前の ScanInput には見えない場所から取ってきます。そもそも Scan を避けるべき場面については Query と Scan の比較を参照してください。

コード

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

HasMorePages() が実際に返すもの

約 3.9 KB のアイテム 600 曲のフィクスチャに対して、そのうち 8 件が Year >= 2010 に一致し、末尾にソートされる場合。

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() は最初の呼び出し前から true なので、ループは必ず 1 回は実行されます。テーブルが空かどうかの判定には使えません。さらに 2 ページはスキャンが続いている最中に 0 件を返しており、つまり if len(page.Items) == 0 { break } は 8 件の一致を持つテーブルに対して空の結果を報告します。そしてページネーターは使い捨てです。ループの後、その値は使い切られており、NextPage は再開ではなくエラーを返します。もう一度スキャンするには NewScanPaginator をもう一度呼びます。

Limit は 2 か所にあり、オプションのほうが勝ちます

Go はページサイズを入力構造体とページネーター自身のオプションに分けています。両方を設定するとオプションが優先されます。

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

in.Limit = aws.Int32(500) を代入して 500 件のページを期待するのは、どこか別の場所の関数オプションがすでに 25 を設定していると静かに外れます。ScanInput だけに触れているなら、ページネーターはそれを尊重します。

2026-07-28 に、go1.26.5 上の aws-sdk-go-v2/service/dynamodb v1.62.1 で、DynamoDB Local(amazon/dynamodb-local)に対して計測しました。

解説

  • page.Items は自分の構造体ではなく []map[string]types.AttributeValue ですattributevalue.UnmarshalListOfMaps(items, &songs) が一度の呼び出しでバッチを変換し、Year を Go の int にします。マッピングは dynamodbav の構造体タグで制御します。
  • ワイヤー上では数値は文字列です。生の値は &types.AttributeValueMemberN{Value: "2010"} で、N メンバーは Go の string を保持します。したがって型付き構造体にアンマーシャルしない限り、あらゆる算術の両側に strconv が居座ります。
  • FilterExpression は読み取りの後に走ります。空の 2 ページがそれぞれ 128.5 ユニットかかっているのはそのためです。API リファレンスはフィルタリングが "does not consume any additional read capacity units" と明言していて、その裏返しは、節約もしないということです。
  • #filter0 は様式ではなく必須ですYear は DynamoDB の予約語で、別名を付けずに使うと ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year が返ります。
  • Segment / TotalSegments は各 goroutine にテーブルの自分のスライスを与えます。それぞれに自分のページネーターが必要です。並列化は実時間を削りますが、消費するキャパシティは同じです。

ビジュアルに行う

DynamoDB クエリビルダーは、フィルター、名前と値のマップ、そしてページネーターのループを含むプログラム全体の形を出力するので、このページが計測している部分は記憶ではなく生成されたものになります。

スキャンをサービスに投げる前に、フィルターが実際にどれだけのアイテムに触れるかを確かめるには、DynoTable をダウンロードして、テーブルに対してグリッドで実行してください。

関連する例

参考資料

このリクエストをビジュアルに組み立てる

この操作を無料の DynamoDB クエリビルダーで組み立て — キー条件、フィルタ、インデックス、Limit、ソート順、ページネーションループ — 実行可能な SDK v3・CLI・boto3 のプログラムとしてコピーして戻れます。

DynamoDB クエリビルダーを開く

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。