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 表达式构建器会把键条件连同配套的 ExpressionAttributeNames 和 ExpressionAttributeValues 一起装配好,并输出 Go 字面量,于是名称和值不会各走各的。
要针对你自己的表运行查询——键条件表单、随滚动自动翻页的网格、把请求作为 Go 代码复制出来——请下载 DynoTable。
相关示例
- Java 中的 DynamoDB Query——用 AWS SDK for Java 2.x 完成同样的查询。
- Go 中的 DynamoDB Scan——当你无法用键定位到一个分区时。
- 分页——
LastEvaluatedKey、ExclusiveStartKey,以及为什么Limit不是页大小。 - "Query condition missed key schema element"——键条件写错了属性名,或者漏掉了分区键。
- "Query key condition not supported"——键条件用不了的运算符,比如 contains 或第二个排序键条件。
参考资料
- Query — Amazon DynamoDB API Reference
- Use Query with an AWS SDK or CLI — Amazon DynamoDB Developer Guide
- dynamodb package — AWS SDK for Go v2 (pkg.go.dev)
- expression package — AWS SDK for Go v2 (pkg.go.dev)
- Querying tables — Amazon DynamoDB Developer Guide
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 应用文档记载的取整规则;请把绝对单元数当作形态演示,规划容量前请针对真实服务测量。