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 25in.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 をダウンロードして、テーブルに対してグリッドで実行してください。
関連する例
- Java での DynamoDB Scan — AWS SDK for Java 2.x による同じスキャン。
- Go での DynamoDB Query — ふつうはこちらに手を伸ばすべき、より安い読み取り。
- Query と Scan の比較 — (まれに)
Scanが正当化されるとき。 - DynamoDB の Scan はなぜ遅くて高いのか? — コストモデルと、その回避方法。
- DynamoDB ProvisionedThroughputExceededException — テーブル全体のスキャンがプロビジョニング済みテーブルのキャパシティに与えるもの。
- DynamoDB ThrottlingException — もう一方のスロットリングと、指数バックオフでの扱い方。