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`);

解説

  • オプショナルチェーンは防御的なノイズではありませんResponsesUnprocessedKeys も 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" を使うよう指示しています。即座にリトライすると、スロットリングされている同じパーティションに当たるからです。
  • ConsistentReadProjectionExpression はテーブル単位です。トップレベルではなく 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 をダウンロードしてください。

関連する例

参考資料

最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。