Node.js での DynamoDB BatchGetItem(AWS SDK v3)
BatchGetItem は プライマリキーで最大 100 件のアイテム を 1 リクエストで取得します。AWS SDK v3 ではこの呼び出しをループにする必要があります。UnprocessedKeys がエラーではなく 成功 レスポンスに乗って返ってくるからです。何がそこに入るのか、そしてなぜ 16 MB と パーティションあたり 1 MB が効いてくる数字なのかは、DynamoDB のバッチ操作で扱っています。このページは v3 の呼び出しと、それが返してくるものについてです。
コード
import {BatchGetItemCommand, DynamoDBClient} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'us-east-1'});
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let requestItems = {
Music: {
Keys: [
{Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'Cubano Chant'}},
{Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'A Mis Abuelos'}},
{Artist: {S: 'Ella Fitzgerald'}, SongTitle: {S: 'Misty'}}
]
}
};
const items = [];
let attempt = 0;
do {
const response = await client.send(new BatchGetItemCommand({RequestItems: requestItems}));
items.push(...(response.Responses?.Music ?? []));
// A partial result is NOT an error: throttling, a >16 MB response, or an
// internal failure returns the leftovers in UnprocessedKeys. Retry them
// with exponential backoff.
requestItems = response.UnprocessedKeys;
if (requestItems && Object.keys(requestItems).length > 0) {
attempt += 1;
await sleep(Math.min(100 * 2 ** attempt, 5000));
}
} while (requestItems && Object.keys(requestItems).length > 0);
console.log(`Fetched ${items.length} items`);解説
- オプショナルチェーンは防御的なノイズではありません。
ResponsesもUnprocessedKeysも v3 の型では省略可能なので、response.Responses?.Music ?? []とObject.keys()のガードはコンパイラが要求するものです。素の JavaScript では、最初の空レスポンスで例外が投げられるのを止めているのがこれです。 err.nameで分岐します。v3 はサービスのエラーコードをそこに入れます。そしてこのコマンドが実際に投げる 2 つの失敗はリトライ不可なので、決してバックオフループに落としてはいけません。キーが 100 件を超えるとValidationException/Too many items requested for the BatchGetItem callになり、同じキーを 2 回入れるとProvided list of item keys contains duplicatesになります。どちらも Python のページに逐語で再現しています。UnprocessedKeysはすでにRequestItemsの形で返ってきます。ループがそれをそのまま代入し直せる唯一の理由がそれです。ページネーションのカーソルではありませんし、呼び出しが失敗したという意味でもありません。- バックオフは気の利いた配慮ではなく AWS の指示です。API リファレンスは "an exponential backoff algorithm" を使うよう指示しています。即座にリトライすると、スロットリングされている同じパーティションに当たるからです。
ConsistentReadとProjectionExpressionはテーブル単位です。トップレベルではなくRequestItemsの各エントリの中に設定します。マップのキーが 1 つでフラットなリクエストに見えるときは、見落としやすいところです。
レスポンスが実際に返ってくる形
3 曲すべてが存在する DynamoDB Local 3.3.0 に対して上のコードを実行し、ReturnConsumedCapacity: 'TOTAL' を加えて、件数の代わりに曲名をログ出力すると:
order: [ 'A Mis Abuelos', 'Misty', 'Cubano Chant' ]
UnprocessedKeys: {}
ConsumedCapacity: [ { TableName: 'Music', CapacityUnits: 1.5 } ]リクエストは Cubano Chant、A Mis Abuelos、Misty の順に並べていました。レスポンスはそのどの位置とも一致していません。ループがオフセットで添字を取らずフラットな配列に push しているのはそのためです。アイテムはキー属性でリクエストに突き合わせ、突き合わせるものが残るように ProjectionExpression にはそのキーを含めてください。
同じ Music エントリに ConsistentRead: true を設定すると、同じ 3 つのキーは 1.5 ではなく 3 ユニットかかります。それぞれ 4 KB 未満の 3 件のアイテムは、3 回の個別の GetItem 読み取りとして課金されます。結果整合性なら 0.5 ユニット、強い整合性なら 1 ユニットです。料金計算ツールは、読み取りパターンを決めてしまう前に、そのアイテム単位の計算を月額の数字に変換します。
ここで Misty を削除して再実行すると、2 件のアイテム、空の UnprocessedKeys、そして 1.0 ユニットになります。存在しないキーには何も課金されませんでした。これは DynamoDB Local の挙動であって契約ではありません。BatchGetItem のリファレンス(2026-07-28 取得)は、存在しないアイテムへのリクエストもその読み取り種別に応じた最小の読み取りキャパシティを消費すると述べています。キャッシュミスばかりのバッチをローカル実行から見積もらないでください。
キーの集合を引き戻して実際に何が返ってきたかを、ループを先に書かずに確認するには、DynoTable をダウンロードしてください。
関連する例
- Python での DynamoDB BatchGetItem — boto3 による同じバッチ読み取り。
- AWS CLI での DynamoDB BatchGetItem — シェルからの同じバッチ読み取り。
- Node.js での DynamoDB GetItem — これがバッチ化している単一アイテムの読み取り。
- DynamoDB のバッチ操作 — 上限、部分的な失敗、そしてバッチ化が報われる場面。
- 「Too many items requested for the BatchGetItem call」 — 1 リクエストに 100 件を超えるキー。
- 「Provided list of item keys contains duplicates」 — 1 つのバッチに同じキーが 2 回。
参考資料
- BatchGetItem — Amazon DynamoDB API Reference
- Error handling with DynamoDB — Amazon DynamoDB Developer Guide
- DynamoDB read and write operations (capacity unit consumption) — Amazon DynamoDB Developer Guide
最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。