Go 中的 DynamoDB Scan(AWS SDK v2)

NewScanPaginator 是一个游标,不是一个集合:它一开始乐观,最后耗尽,而且它的页大小取自一个你眼前这个 ScanInput 并不显示的地方。至于什么时候该彻底避开 Scan,参见 Query vs. 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() 实际返回的是什么

针对一份 600 首歌、项目约 3.9 KB 的固定数据,其中有 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,所以这个循环总会至少跑一趟。你不能用它来判断一张表是不是空的。接着有两页在扫描仍在进行时返回了零个项目,这意味着 if len(page.Items) == 0 { break } 会在一张有 8 个匹配项的表上报告空结果。而且这个分页器是一次性的:循环结束后这个值就用完了,NextPage 会返回一个错误而不是重新开始。要再扫一遍,就得再调一次 NewScanPaginator

Limit 住在两个地方,而选项那个赢

Go 把页大小拆在了输入结构体和分页器自己的选项之间。两边都设,选项优先:

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

当别处某个函数式选项已经把它设成 25 时,赋值 in.Limit = aws.Int32(500) 还指望拿到 500 项一页,就是一次悄无声息的落空。如果你只碰 ScanInput,分页器是听它的。

2026-07-28 针对 9000 端口上的 DynamoDB Local(amazon/dynamodb-local),使用 go1.26.5 上的 aws-sdk-go-v2/service/dynamodb v1.62.1 实测。

说明

  • page.Items[]map[string]types.AttributeValue,不是你的结构体。attributevalue.UnmarshalListOfMaps(items, &songs) 一次调用就能转换整批,并把 Year 变成 Go 的 int;映射关系由 dynamodbav 结构体标签控制。
  • 数字在传输时是字符串。原始值是 &types.AttributeValueMemberN{Value: "2010"}——N 这个成员装的是 Go 的 string,所以除非你反序列化进一个带类型的结构体,否则任何算术的两侧都得摆上 strconv
  • FilterExpression 是在读取之后才跑的,这正是那两个空页面照样各花 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 查询构建器

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。