Go 中的 DynamoDB Query(AWS SDK v2)

Query 读取一个分区,可选地用排序键收窄范围(Query vs Scan 讲了什么时候该这么选,键条件表达式列出了每一个合法运算符)。AWS SDK for Go v2 额外提供的是 QueryPaginator,它把 LastEvaluatedKey 循环变成了一个 for 循环,同时也悄悄拿走了你叫停的机会。

代码

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.NewQueryPaginator(client, &dynamodb.QueryInput{
		TableName: aws.String("Music"),
		KeyConditionExpression: aws.String(
			"#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)"),
		ExpressionAttributeNames: map[string]string{
			"#hashKey":  "Artist",
			"#rangeKey": "SongTitle",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":hashKeyValue":  &types.AttributeValueMemberS{Value: "Arturo Sandoval"},
			":rangeKeyValue": &types.AttributeValueMemberS{Value: "C"},
		},
	})

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

说明

Limit 并不能限制分页器读多少。这一条是要花钱的。给上面的输入设上 Limit: aws.Int32(5),在一个含 30 个各约 60 KB 项目的分区上运行,得到:

page 1: Count=5 CU=37.0   page 5: Count=5 CU=37.0
page 2: Count=5 CU=37.0   page 6: Count=5 CU=37.0
page 3: Count=5 CU=37.0   page 7: Count=0 CU=0.0
page 4: Count=5 CU=37.0   total: 30 items, 222.0 units

三十个项目全回来了。Limit页大小,而分页器的职责就是一直问到没页可问为止,于是两者正好互相抵消。AWS 把它定义为「要求值的最大项目数(不一定是匹配项目的数量)」(抓取于 2026-07-28)。如果你只想要前五个项目,请在第一页之后自己 break 出循环。

小页面稍微更贵,而不是更便宜。同一个分区不带 Limit 读取时会分成两页、共 220.0 个单元;设成 Limit: 5 则用了七次调用、共 222.0。每一页都会把自己的字节总量向上取整到下一个 4 KB 边界,所以页越多、取整越多,还要多付六次往返的延迟。为了「少读一点」而调低 Limit,两头都落空。

这个循环总会比数据多发一次调用。上面的第 7 页返回了零个项目。只要达到了 Limit,DynamoDB 就会交回一个 LastEvaluatedKey,不管后面还有没有数据,而 HasMorePages() 就信了它。所以一个设了 Limit 的分页器会以一次白费的请求收尾,任何按页触发的副作用(进度条、批量刷新、日志行)都会在一个空页上多触发一次。请用 len(page.Items) 做防护。

在第一次请求之前 HasMorePages() 就是 true。它被初始化为 true,好让 for 循环至少能进得去,这意味着它是循环条件,而不是「有没有数据」的检查。用它来决定要不要查询,答案永远是「要」。

错误是按页浮现的,而部分读取是一种真实状态NextPage 返回的是和其他调用一样的被包装的 smithy 错误,所以请用 errors.As*types.ProvisionedThroughputExceededException 之类做解包,而不是匹配字符串。代码片段里的 log.Fatalf 会把已经收集到的页全部丢掉;在真实服务里,你通常想保留 items 并报告自己走到了哪一步。

ScanIndexForward 是倒着读一个分区的唯一办法。设 ScanIndexForward: aws.Bool(false) 得到排序键的降序。除此之外没有任何「按某某排序」:顺序来自排序键,如果你需要另一种顺序,你需要另一个索引。IndexName: aws.String("...") 会把整个查询挪到那个索引上。

有两个包能让这段代码更短github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression 能从 expression.Key("Artist").Equal(expression.Value("Arturo Sandoval")) 构建出键条件和两个占位符映射,从而免去手写的 #hashKey/:hashKeyValue 配对,也连带免去保留字的风险。attributevalue.UnmarshalListOfMaps(page.Items, &songs) 则把一页直接变成 []Song

用可视化的方式来做

那两个占位符映射,才是值得生成而不是手敲的部分。免费的 DynamoDB 表达式构建器会把键条件连同配套的 ExpressionAttributeNamesExpressionAttributeValues 一起装配好,并输出 Go 字面量,于是名称和值不会各走各的。

要针对你自己的表运行查询——键条件表单、随滚动自动翻页的网格、把请求作为 Go 代码复制出来——请下载 DynoTable

相关示例

参考资料

2026-07-28 在 go1.26.5 上用 aws-sdk-go-v2/service/dynamodb v1.62.1 针对 9000 端口上的 DynamoDB Local(amazon/dynamodb-local)实测,数据集为一个含 30 个各约 60 KB 项目的分区。上方每页的计数与容量读数均为原样照录的输出。DynamoDB Local 应用文档记载的取整规则;请把绝对单元数当作形态演示,规划容量前请针对真实服务测量。

可视化构建此请求

在免费的 DynamoDB 查询构建器中组装此操作 —— 键条件、筛选、索引、Limit、排序方向和分页循环 —— 再把它作为可运行的 SDK v3、CLI 或 boto3 程序复制回来。

打开 DynamoDB 查询构建器

无需控制台即可使用 DynamoDB

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

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