DynamoDB Query in Go (AWS SDK v2)
Query reads one partition, optionally narrowed by the sort key (Query vs Scan covers when that is the right call, and key condition expressions lists every legal operator). What AWS SDK for Go v2 adds is QueryPaginator, which turns the LastEvaluatedKey loop into a for loop and, in doing so, quietly removes your chance to stop.
Code
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))
}Explanation
Limit does not bound what the paginator reads. This is the one that costs money. Setting Limit: aws.Int32(5) on the input above and running it over a partition of 30 items at ~60 KB each produced:
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 unitsAll thirty items came back. Limit is a page size, and the paginator's job is to keep asking until the pages run out, so the two cancel each other exactly. AWS defines it as "the maximum number of items to evaluate (not necessarily the number of matching items)" (fetched 2026-07-28). If you want the first five items, break out of the loop yourself after the first page.
Small pages cost slightly more, not less. The same partition read without Limit paginates in two pages for 220.0 units; at Limit: 5 it took seven calls for 222.0. Each page rounds its own byte total up to the next 4 KB boundary, so more pages means more rounding, plus six extra round trips of latency. Lowering Limit to "read less" gets you neither.
The loop always makes one more call than there is data. Page 7 above returned zero items. DynamoDB hands back a LastEvaluatedKey whenever Limit was reached, whether or not anything follows, and HasMorePages() believes it. So a Limit-ed paginator ends on a wasted request, and any per-page side effect (a progress bar, a batch flush, a log line) fires once against an empty page. Guard on len(page.Items).
HasMorePages() is true before the first request. It is initialised to true so the for loop enters at all, which means it is a loop condition, not a "has data" check. Calling it to decide whether to bother querying always says yes.
Errors surface per page, and a partial read is a real state. NextPage returns the same wrapped smithy error as any other call, so unwrap with errors.As against *types.ProvisionedThroughputExceededException and friends rather than matching strings. The snippet's log.Fatalf throws away the pages already collected; in a service you usually want to keep items and report how far you got.
ScanIndexForward is the only way to read a partition backwards. Set ScanIndexForward: aws.Bool(false) for descending sort-key order. There is no "sort by" for anything else: order comes from the sort key, and if you need another order you need another index. IndexName: aws.String("...") moves the whole query onto that index.
Two packages make this shorter. github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression builds the key condition and both placeholder maps from expression.Key("Artist").Equal(expression.Value("Arturo Sandoval")), which removes the hand-written #hashKey/:hashKeyValue pairs and the reserved-word risk with them. attributevalue.UnmarshalListOfMaps(page.Items, &songs) turns a page straight into []Song.
Do it visually
The two placeholder maps are the part worth generating rather than typing. The free DynamoDB Expression Builder assembles the key condition with matching ExpressionAttributeNames and ExpressionAttributeValues and emits the Go literal, so the names and values cannot drift apart.
To run queries against your own tables — key-condition form, a grid that pages as you scroll, copy the request back out as Go — download DynoTable.
Related examples
- DynamoDB Query in Java — the same query with AWS SDK for Java 2.x.
- DynamoDB Scan in Go — when you can't key into a partition.
- Pagination —
LastEvaluatedKey,ExclusiveStartKey, and whyLimitis not a page size. - "Query condition missed key schema element" — the key condition names the wrong attribute or skips the partition key.
- "Query key condition not supported" — an operator the key condition can't use, like contains or a second sort-key condition.
References
- 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
Measured 2026-07-28 on go1.26.5 with aws-sdk-go-v2/service/dynamodb v1.62.1, against DynamoDB Local (amazon/dynamodb-local) on port 9000, over a partition of 30 items at ~60 KB each. The per-page counts and capacity readings are captured output. DynamoDB Local applies the documented rounding rules; treat the absolute units as a demonstration of the shape and measure the service before sizing.