Java 中的 DynamoDB Query(AWS SDK v2)

AWS SDK for Java 2.x 裡的 queryPaginator 看起來像一個集合,但它不是。它是一個惰性、可重複迭代的物件,而這個差別會在你第二次迴圈它時顯示在你的帳單上。至於 Query 究竟何時才是對的讀取方式,請見 Query 與 Scan 的比較

程式碼

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;

public class QueryExample {
    public static void main(String[] args) {
        try (DynamoDbClient ddb = DynamoDbClient.builder()
                .region(Region.US_EAST_1)
                .build()) {

            Map<String, String> names = new HashMap<>();
            names.put("#hashKey", "Artist");
            names.put("#rangeKey", "SongTitle");

            Map<String, AttributeValue> values = new HashMap<>();
            values.put(":hashKeyValue", AttributeValue.builder().s("Arturo Sandoval").build());
            values.put(":rangeKeyValue", AttributeValue.builder().s("C").build());

            QueryRequest request = QueryRequest.builder()
                    .tableName("Music")
                    .keyConditionExpression(
                            "#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)")
                    .expressionAttributeNames(names)
                    .expressionAttributeValues(values)
                    .build();

            List<Map<String, AttributeValue>> items = new ArrayList<>();
            for (QueryResponse page : ddb.queryPaginator(request)) {
                items.addAll(page.items());
            }
            System.out.println("Found " + items.size() + " items");
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

QueryIterable 每次迭代都會重跑一次查詢

針對一個 600 首歌的樣本資料、每首歌約 3.9 KB 且全都在 Artist = "Arturo Sandoval" 之下,ddb.queryPaginator(request) 在 2.6 毫秒內回傳,而且什麼都沒送出。接著同一個 QueryIterable 物件被迴圈了兩次:

queryPaginator(request) returned in 2.60 ms   (class QueryIterable, 0 requests)
iteration 1: pages=3  items=600  capacity=284.5
iteration 2: pages=3  items=600  capacity=284.5   <- same object

在一個變數上跑兩個 for 迴圈,569 個讀取單位。那些頁面從來沒有被快取;每次迭代都會從頭再走一遍 LastEvaluatedKey。如果你需要那些項目兩次,就像範例那樣,把可迭代物件一次性倒進一個 List 裡。

同一種惰性的另一面,是錯誤浮現的位置。建構一個索引鍵條件漏掉分割區索引鍵的請求,queryPaginator 會毫無怨言地接受,因為還沒有任何呼叫發生:

queryPaginator(bad) constructed without throwing
threw on iteration: DynamoDbException / ValidationException /
Query condition missed key schema element / http 400

包在 builder 外面的 try-catch 什麼也捕捉不到。它必須包住那個迴圈,這也是為什麼範例把整段程式碼放進同一個 try 裡。

已於 2026-07-28 對照 9000 埠上的 DynamoDB Local(amazon/dynamodb-local),使用 OpenJDK 26.0.1 上的 software.amazon.awssdk:dynamodb 2.49.4 實測。

說明

  • ddb.queryPaginator(request).items() 會把那些頁面攤平成一個 Iterable<Map<String, AttributeValue>> 並在你背後分頁,所以當你只想要項目時,範例裡的兩層迴圈就縮成一層。它同時也是一個 SdkIterable,所以 .stream() 可用。
  • 在 builder 裡數字是 StringAttributeValue.builder().n("1994") 不是 .n(1994) 的打字錯誤 — n() setter 收的是 java.lang.String,因為 DynamoDB 是以十進位文字傳輸數字,以避免二進位浮點的捨入。傳一個 Java int 進去根本編譯不過。
  • keyConditionExpression 接受一個分割區索引鍵上的等式,加上最多一個排序索引鍵條件(=<<=>>=BETWEENbegins_with);.scanIndexForward(false) 會反轉順序,而 .indexName("...") 會改指向某個次要索引。
  • Enhanced Client 是另一種使用手感software.amazon.awssdk.enhanced.dynamodb 對應的是加了註解的 bean,而不是 Map<String, AttributeValue>,而它的 query 回傳一個 PageIterable<T>,具有上面實測到的同樣惰性重複迭代語意。

改用視覺化操作

這個範例裡的 #hashKey#rangeKey 都是沒有人強迫你用的別名:ArtistSongTitle 都不在 AWS 那份 573 字的保留字清單上。保留字檢查器會告訴你,你的哪些屬性名稱真的需要 # 這套處理,讓別名對應不再只是照本宣科。

想在建構 QueryRequest 之前,先針對一張真實資料表試一個索引鍵條件,就下載 DynoTable,並在格線中翻閱結果。

相關範例

參考資料

以視覺化方式建構此請求

在免費的 DynamoDB 查詢建構器中組合此操作 — 鍵條件、Filter、Index、Limit、排序方向與分頁迴圈 — 再把它複製成可執行的 SDK v3、CLI 或 boto3 程式。

開啟 DynamoDB 查詢建構器

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

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

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