Node.js での DynamoDB Scan(AWS SDK v3)
以下の do/while は防御的なコーディングではありません。フィルター付きの Scan のページは空の Items 配列を返しながら、テーブルにまだ続きが残っていることがあります。つまり最初のレスポンスで止めるのは、一致があるテーブルに対してスキャンが 0 件を報告する方法そのものです。この操作を完全に避けるべき場面は Query と Scan の比較で扱っています。
コード
import {DynamoDBClient, ScanCommand} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'us-east-1'});
const items = [];
let lastEvaluatedKey;
do {
const response = await client.send(
new ScanCommand({
TableName: 'Music',
FilterExpression: '#filter0 >= :filterValue0',
ExpressionAttributeNames: {
'#filter0': 'Year'
},
ExpressionAttributeValues: {
':filterValue0': {N: '2010'}
},
ExclusiveStartKey: lastEvaluatedKey
})
);
items.push(...(response.Items ?? []));
lastEvaluatedKey = response.LastEvaluatedKey;
} while (lastEvaluatedKey);
console.log(`Matched ${items.length} items`);空のページ 2 つ、284.5 読み取りユニット、8 件のアイテム
フィクスチャはそれぞれ約 3.9 KB の 600 曲で、そのうちちょうど 8 件が Year >= 2010 を満たし、末尾にソートされます。上のループが実際に受け取るのはこれです。
| ラウンドトリップ | Items.length | ScannedCount | 読み取りユニット | LastEvaluatedKey |
|---|---|---|---|---|
| 1 | 0 | 271 | 128.5 | あり |
| 2 | 0 | 271 | 128.5 | あり |
| 3 | 8 | 58 | 27.5 | なし |
連続する 2 ページが何も返さず、それぞれ 128.5 読み取りユニットかかっています。if (!response.Items.length) return とするコードは空のテーブルだと報告します。API リファレンスはその規則を端的に述べています。"a scan result can result in no items meeting the criteria and the Count will result in zero"、そして別途 "a FilterExpression is applied after the items have already been read; the process of filtering does not consume any additional read capacity units" と。
2 つ目の文は請求書の目線で読んでください。フィルターは無料で、それが捨てたものは無料ではありません。8 件のアイテムを届けるのに 284.5 読み取りユニット、フィルターがまったくない場合と同じ請求です。
2026-07-28 に、node v24.18.0 上の @aws-sdk/client-dynamodb 3.1095.0 で、DynamoDB Local(amazon/dynamodb-local)に対して計測しました。件数とキャパシティはエンジン自身のレスポンスフィールドです。
解説
ExclusiveStartKey: lastEvaluatedKeyは初回はundefinedです。v3 のシリアライザーはundefinedのメンバーを落とすので、1 つのオブジェクトリテラルで最初のリクエストとその後のすべてを賄えます。代わりに{}を渡すとValidationException: The provided starting key is invalidで失敗します。response.Items ?? []は実際に仕事をしています。上の表と併せて読んでください。この null 合体は何も一致しなかったページでもアキュムレーターを正しく保ち、whileはそのページを越えてループを生かし続けます。#filter0は飾りではありません。Yearは AWS の予約語リストに載っており、別名を付けずに使うとValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Yearが返ります。Limitが数えるのは読んだアイテムであって返すアイテムではありません。このフィルターではLimit: 10はCount: 0とScannedCount: 10を返します。これはキャパシティのスパイクを抑えるつまみであって、結果を 10 件くださいと頼む方法ではありません。Segment/TotalSegmentsはテーブル全体のスキャンをワーカーに分割します。分割されるのは実時間であってコストではありません。同じ 284.5 ユニットを、より速く、より並行に使うだけです。
実際のテーブルではいくらになるか
8 件のアイテムに 284.5 読み取りユニットというのが問題の形であり、これは結果ではなくテーブルに比例して増えます。ホットパスにフィルター付きのスキャンを載せる前に、自分のアイテムサイズとトラフィックでテーブル全件読み取りの値段を DynamoDB 料金計算ツールで出し、同じアクセスパターンを Query に変える GSI と比べてください。
テーブルを GUI で、フィルター付き・ページ送り可能な結果グリッドとともに探索するには、スクリプトから手探りでスキャンする代わりに DynoTable をダウンロードしてください。
関連ガイド
- Query と Scan の比較 — (まれに)
Scanが正当化されるとき。 - DynamoDB の Scan はなぜ遅くて高いのか? — コストモデルと、その回避方法。
- DynamoDB ProvisionedThroughputExceededException — テーブル全体のスキャンがプロビジョニング済みテーブルのキャパシティに与えるもの。
- DynamoDB ThrottlingException — もう一方のスロットリングと、指数バックオフでの扱い方。