Go 中的 DynamoDB Query(AWS SDK v2)

Query 會讀取一個分割區,並可選擇用排序索引鍵再收窄(Query 與 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 把它定義為「the maximum number of items to evaluate (not necessarily the number of matching items)」(擷取於 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 迴圈至少進得去 — 也就是說它是迴圈條件,不是「有沒有資料」的檢查。拿它來決定要不要查詢,答案永遠是 yes。

錯誤是逐頁浮現的,而讀到一半是一種真實狀態NextPage 回傳的是與其他呼叫一樣、被 smithy 包過的錯誤,所以請用 errors.As*types.ProvisionedThroughputExceededException 之類的型別解包,而不是比對字串。片段裡的 log.Fatalf 會把已經蒐集到的頁面全丟掉;在服務裡,你通常會想保住 items 並回報自己走到了哪裡。

ScanIndexForward 是唯一能反向讀取分割區的方法。設 ScanIndexForward: aws.Bool(false) 就是排序索引鍵由大到小。除此之外沒有任何「sort by」:順序來自排序索引鍵,需要別的順序就得再開一個索引。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 Expression Builder 會把鍵條件與相符的 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 查詢建構器中組合此操作 — 鍵條件、Filter、Index、Limit、排序方向與分頁迴圈 — 再把它複製成可執行的 SDK v3、CLI 或 boto3 程式。

開啟 DynamoDB 查詢建構器

不必透過主控台就能操作 DynamoDB

一款快速的 DynamoDB 桌面用戶端,可執行 DynamoDB 無法執行的真正 SQL — JOINs、GROUP BY、聚合 — 並支援視覺化編輯與使用你自己的 Bedrock 金鑰的 AI 代理。

30 天免費試用,無需信用卡 — 之後為無時間限制的免費方案。