DynamoDBのScan vs Query(GSI)を100万件まで検証して、性能に差が出るラインを調べてみた
人材育成室 育成メンバーチームで 研修中の はす です。
DynamoDB の初学者にとって、「Scanは全データを読み込むので、できるだけセカンダリインデックスを活用してコストを抑えなければ!」 という意識が強くありました。しかし、実際に 数百件〜数万件規模 でどれほどの差が出るのかを比較した経験がなく、設計を行う際に自信を持てませんでした。
そこで、実際にデータ規模を変えて、パフォーマンスとコストにどのような差が出るのかを検証してみました。
読者ターゲット
- DynamoDB の設計で Scan と GSI の使い分けに迷っている方
- 「Scanは遅い」とは聞くけれど、具体的に何件くらいから厳しくなるのかを知りたい方
前提
今回対象とするのは、DynamoDB オンデマンドモード 結果整合性のある読み込み(デフォルト)についてのみです。
検証方法
小規模から大規模な環境を再現するために、以下のような 「データサイズ」 「データ件数」 「比較手法」 をそれぞれ組み合わせた 計30パターン で検証します。
| 項目 | 対象 |
|---|---|
| データサイズ | 0.5KB / 1KB / 5KB |
| データ件数 | 100 / 1,000 / 10,000 / 100,000 / 1,000,000 件 |
| 比較手法 | Scan + FilterExpression vs Query (GSI) |
※ LSIを検証に含めなかった理由
LSI は同一パーティションキー内でのみ機能します。本テーブルのようにPKが 「商品ID」 の場合、異なる商品ID を跨いだ 「カテゴリ検索」 には利用できないため、今回は除外しました。
計測環境と計測方法
実行環境:AWS Lambda (Runtime: Node.js 24.x / Memory: 1024MB)
計測方法:各パターンにつき計5回の計測を行い、その平均値を使用
各リソースについて
DynamoDB テーブルはデータサイズとデータ件数ごとに、3 x 5 パターンで計15個用意します。

DynamoDBの設定
// 必要な箇所のみ抜粋しています
const ITEM_COUNTS = [100, 1000, 10000, 100000, 1000000];
const RECORD_SIZES = [0.5, 1, 5]; // KB
const tables = ITEM_COUNTS.flatMap((itemCount) =>
RECORD_SIZES.map((recordSize) => {
const recordSizeLabel = recordSize === 0.5 ? "0_5" : String(recordSize);
const tableId = `ProductsTable${itemCount}_${recordSizeLabel}kb`;
const tableName = `Products-${itemCount}-${recordSize}kb`
const table = new dynamodb.Table(this, tableId, {
tableName,
partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
})
table.addGlobalSecondaryIndex({
indexName: "category-name-index",
partitionKey: {
name: "category",
type: dynamodb.AttributeType.STRING,
},
sortKey: { name: "name", type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
})
return table;
}),
);
Lambda関数はScan と Query 用で用意します。

Lambda関数の設定
// 必要な箇所のみ抜粋しています
...
const commonLambdaProps = {
runtime: lambda.Runtime.NODEJS_24_X,
memorySize: 1024,
bundling: {
minify: true,
sourceMap: false,
},
tracing: lambda.Tracing.ACTIVE,
}
const testScanFunction = new NodejsFunction(this, "TestScanFunction", {
...commonLambdaProps,
functionName: "BenchmarkStack-TestScanFunction",
entry: path.join(__dirname, "../functions/test-scan.ts"),
handler: "handler",
timeout: cdk.Duration.minutes(15),
})
const testQueryFunction = new NodejsFunction(this, "TestQueryFunction", {
...commonLambdaProps,
functionName: "BenchmarkStack-TestQueryFunction",
entry: path.join(__dirname, "../functions/test-query.ts"),
handler: "handler",
timeout: cdk.Duration.minutes(15),
});
検証結果
以下のような結果となりました
Record Size: 0.5KB
| Item Count | Scan Time | Query Time | Scan RCU | Query RCU | Scan Pages | Query Pages |
|---|---|---|---|---|---|---|
| 100 | 13ms (1.9x) | 7ms | 7 (3.5x) | 2 | 1 (1.0x) | 1 |
| 1,000 | 40ms (1.4x) | 28ms | 58 (3.2x) | 18 | 1 (1.0x) | 1 |
| 10,000 | 169ms (1.1x) | 151ms | 571 (3.3x) | 174 | 5 (2.5x) | 2 |
| 100,000 | 1,668ms (1.5x) | 1,104ms | 5,687 (3.3x) | 1,727 | 45 (3.0x) | 15 |
| 1,000,000 | 15,917ms (1.6x) | 9,849ms | 56,738 (3.3x) | 17,234 | 442 (3.1x) | 142 |
Record Size: 1KB
| Item Count | Scan Time | Query Time | Scan RCU | Query RCU | Scan Pages | Query Pages |
|---|---|---|---|---|---|---|
| 100 | 14ms (1.8x) | 8ms | 13 (3.3x) | 4 | 1 (1.0x) | 1 |
| 1,000 | 30ms (1.2x) | 26ms | 121 (3.3x) | 37 | 1 (1.0x) | 1 |
| 10,000 | 256ms (1.3x) | 195ms | 1,198 (3.3x) | 362 | 10 (3.3x) | 3 |
| 100,000 | 2,488ms (1.4x) | 1,757ms | 11,969 (3.3x) | 3,612 | 94 (3.2x) | 29 |
| 1,000,000 | 26,327ms (1.4x) | 18,709ms | 119,554 (3.3x) | 36,094 | 931 (3.2x) | 288 |
Record Size: 5KB
| Item Count | Scan Time | Query Time | Scan RCU | Query RCU | Scan Pages | Query Pages |
|---|---|---|---|---|---|---|
| 100 | 17ms (1.3x) | 13ms | 63 (3.3x) | 19 | 1 (1.0x) | 1 |
| 1,000 | 97ms (1.2x) | 83ms | 622 (3.3x) | 187 | 5 (2.5x) | 2 |
| 10,000 | 1,041ms (1.7x) | 612ms | 6,208 (3.3x) | 1,863 | 49 (3.3x) | 15 |
| 100,000 | 10,495ms (2.0x) | 5,167ms | 62,079 (3.3x) | 18,626 | 484 (3.3x) | 148 |
| 1,000,000 | 137,374ms (2.5x) | 54,086ms | 620,773 (3.3x) | 186,163 | 4,831 (3.3x) | 1,479 |
レスポンス時間
ScanとQueryの時間の差を 倍率 で見ると、レコードサイズによらず 1.1 〜 2.5 倍程度 と大きな差は出なかったものの、時間の差 は顕著になりました。
0.5KB × 100万件 : Scan 16秒 vs Query 10秒(差: 約6秒)
5KB × 100万件 : Scan 137秒 vs Query 54秒(差: 約83秒)
とはいえ、6秒の差でもユーザー向けアプリケーションなどではUXに大きな影響が出ます。
1秒以上の差があるパターンでは、Scan 以外の選択肢を検討した方が良さそうです。
詳細な検証結果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 100 件 / レコードサイズ: 0.5KB(テーブル: Products-100-0.5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 125ms | RCU: 7 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 15ms | RCU: 7 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 14ms | RCU: 7 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 13ms | RCU: 7 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 14ms | RCU: 7 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 11ms | RCU: 7 | ページ: 1回 | スキャン: 100件 | 結果: 30件
【Query (GSI)】
時間: 115ms | RCU: 2 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 8ms | RCU: 2 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 8ms | RCU: 2 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 7ms | RCU: 2 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 8ms | RCU: 2 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 6ms | RCU: 2 | ページ: 1回 | スキャン: 30件 | 結果: 30件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 100 件 / レコードサイズ: 1KB(テーブル: Products-100-1kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 17ms | RCU: 12.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 12ms | RCU: 12.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 13ms | RCU: 12.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 12ms | RCU: 12.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 13ms | RCU: 12.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 21ms | RCU: 12.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
【Query (GSI)】
時間: 10ms | RCU: 4 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 7ms | RCU: 4 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 8ms | RCU: 4 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 7ms | RCU: 4 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 7ms | RCU: 4 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 10ms | RCU: 4 | ページ: 1回 | スキャン: 30件 | 結果: 30件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 100 件 / レコードサイズ: 5KB(テーブル: Products-100-5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 23ms | RCU: 62.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 20ms | RCU: 62.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 16ms | RCU: 62.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 16ms | RCU: 62.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 16ms | RCU: 62.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
時間: 16ms | RCU: 62.5 | ページ: 1回 | スキャン: 100件 | 結果: 30件
【Query (GSI)】
時間: 14ms | RCU: 19 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 12ms | RCU: 19 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 11ms | RCU: 19 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 11ms | RCU: 19 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 22ms | RCU: 19 | ページ: 1回 | スキャン: 30件 | 結果: 30件
時間: 11ms | RCU: 19 | ページ: 1回 | スキャン: 30件 | 結果: 30件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 1000 件 / レコードサイズ: 0.5KB(テーブル: Products-1000-0.5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 35ms | RCU: 57.5 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 24ms | RCU: 57.5 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 27ms | RCU: 57.5 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 22ms | RCU: 57.5 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 80ms | RCU: 57.5 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 47ms | RCU: 57.5 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
【Query (GSI)】
時間: 22ms | RCU: 17.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 21ms | RCU: 17.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 15ms | RCU: 17.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 16ms | RCU: 17.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 73ms | RCU: 17.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 17ms | RCU: 17.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 1000 件 / レコードサイズ: 1KB(テーブル: Products-1000-1kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 38ms | RCU: 121 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 26ms | RCU: 121 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 25ms | RCU: 121 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 27ms | RCU: 121 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 36ms | RCU: 121 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
時間: 37ms | RCU: 121 | ページ: 1回 | スキャン: 1000件 | 結果: 300件
【Query (GSI)】
時間: 36ms | RCU: 36.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 48ms | RCU: 36.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 24ms | RCU: 36.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 20ms | RCU: 36.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 16ms | RCU: 36.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
時間: 22ms | RCU: 36.5 | ページ: 1回 | スキャン: 300件 | 結果: 300件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 1000 件 / レコードサイズ: 5KB(テーブル: Products-1000-5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 166ms | RCU: 622 | ページ: 5回 | スキャン: 1000件 | 結果: 300件
時間: 110ms | RCU: 622 | ページ: 5回 | スキャン: 1000件 | 結果: 300件
時間: 90ms | RCU: 622 | ページ: 5回 | スキャン: 1000件 | 結果: 300件
時間: 91ms | RCU: 622 | ページ: 5回 | スキャン: 1000件 | 結果: 300件
時間: 96ms | RCU: 622 | ページ: 5回 | スキャン: 1000件 | 結果: 300件
時間: 96ms | RCU: 622 | ページ: 5回 | スキャン: 1000件 | 結果: 300件
【Query (GSI)】
時間: 103ms | RCU: 186.5 | ページ: 2回 | スキャン: 300件 | 結果: 300件
時間: 81ms | RCU: 186.5 | ページ: 2回 | スキャン: 300件 | 結果: 300件
時間: 135ms | RCU: 186.5 | ページ: 2回 | スキャン: 300件 | 結果: 300件
時間: 56ms | RCU: 186.5 | ページ: 2回 | スキャン: 300件 | 結果: 300件
時間: 91ms | RCU: 186.5 | ページ: 2回 | スキャン: 300件 | 結果: 300件
時間: 54ms | RCU: 186.5 | ページ: 2回 | スキャン: 300件 | 結果: 300件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 10000 件 / レコードサイズ: 0.5KB(テーブル: Products-10000-0.5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 239ms | RCU: 570.5 | ページ: 5回 | スキャン: 10000件 | 結果: 3000件
時間: 171ms | RCU: 570.5 | ページ: 5回 | スキャン: 10000件 | 結果: 3000件
時間: 132ms | RCU: 570.5 | ページ: 5回 | スキャン: 10000件 | 結果: 3000件
時間: 184ms | RCU: 570.5 | ページ: 5回 | スキャン: 10000件 | 結果: 3000件
時間: 208ms | RCU: 570.5 | ページ: 5回 | スキャン: 10000件 | 結果: 3000件
時間: 148ms | RCU: 570.5 | ページ: 5回 | スキャン: 10000件 | 結果: 3000件
【Query (GSI)】
時間: 227ms | RCU: 173.5 | ページ: 2回 | スキャン: 3000件 | 結果: 3000件
時間: 144ms | RCU: 173.5 | ページ: 2回 | スキャン: 3000件 | 結果: 3000件
時間: 179ms | RCU: 173.5 | ページ: 2回 | スキャン: 3000件 | 結果: 3000件
時間: 141ms | RCU: 173.5 | ページ: 2回 | スキャン: 3000件 | 結果: 3000件
時間: 168ms | RCU: 173.5 | ページ: 2回 | スキャン: 3000件 | 結果: 3000件
時間: 122ms | RCU: 173.5 | ページ: 2回 | スキャン: 3000件 | 結果: 3000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 10000 件 / レコードサイズ: 1KB(テーブル: Products-10000-1kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 323ms | RCU: 1198 | ページ: 10回 | スキャン: 10000件 | 結果: 3000件
時間: 257ms | RCU: 1198 | ページ: 10回 | スキャン: 10000件 | 結果: 3000件
時間: 259ms | RCU: 1198 | ページ: 10回 | スキャン: 10000件 | 結果: 3000件
時間: 265ms | RCU: 1198 | ページ: 10回 | スキャン: 10000件 | 結果: 3000件
時間: 246ms | RCU: 1198 | ページ: 10回 | スキャン: 10000件 | 結果: 3000件
時間: 255ms | RCU: 1198 | ページ: 10回 | スキャン: 10000件 | 結果: 3000件
【Query (GSI)】
時間: 238ms | RCU: 362 | ページ: 3回 | スキャン: 3000件 | 結果: 3000件
時間: 155ms | RCU: 362 | ページ: 3回 | スキャン: 3000件 | 結果: 3000件
時間: 239ms | RCU: 362 | ページ: 3回 | スキャン: 3000件 | 結果: 3000件
時間: 166ms | RCU: 362 | ページ: 3回 | スキャン: 3000件 | 結果: 3000件
時間: 202ms | RCU: 362 | ページ: 3回 | スキャン: 3000件 | 結果: 3000件
時間: 214ms | RCU: 362 | ページ: 3回 | スキャン: 3000件 | 結果: 3000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 10000 件 / レコードサイズ: 5KB(テーブル: Products-10000-5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 1503ms | RCU: 6208 | ページ: 49回 | スキャン: 10000件 | 結果: 3000件
時間: 1226ms | RCU: 6208 | ページ: 49回 | スキャン: 10000件 | 結果: 3000件
時間: 1175ms | RCU: 6208 | ページ: 49回 | スキャン: 10000件 | 結果: 3000件
時間: 1034ms | RCU: 6208 | ページ: 49回 | スキャン: 10000件 | 結果: 3000件
時間: 886ms | RCU: 6208 | ページ: 49回 | スキャン: 10000件 | 結果: 3000件
時間: 883ms | RCU: 6208 | ページ: 49回 | スキャン: 10000件 | 結果: 3000件
【Query (GSI)】
時間: 648ms | RCU: 1863 | ページ: 15回 | スキャン: 3000件 | 結果: 3000件
時間: 665ms | RCU: 1863 | ページ: 15回 | スキャン: 3000件 | 結果: 3000件
時間: 568ms | RCU: 1863 | ページ: 15回 | スキャン: 3000件 | 結果: 3000件
時間: 668ms | RCU: 1863 | ページ: 15回 | スキャン: 3000件 | 結果: 3000件
時間: 626ms | RCU: 1863 | ページ: 15回 | スキャン: 3000件 | 結果: 3000件
時間: 535ms | RCU: 1863 | ページ: 15回 | スキャン: 3000件 | 結果: 3000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 100000 件 / レコードサイズ: 0.5KB(テーブル: Products-100000-0.5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 1903ms | RCU: 5686.5 | ページ: 45回 | スキャン: 100000件 | 結果: 30000件
時間: 1804ms | RCU: 5686.5 | ページ: 45回 | スキャン: 100000件 | 結果: 30000件
時間: 1727ms | RCU: 5686.5 | ページ: 45回 | スキャン: 100000件 | 結果: 30000件
時間: 1670ms | RCU: 5686.5 | ページ: 45回 | スキャン: 100000件 | 結果: 30000件
時間: 1547ms | RCU: 5686.5 | ページ: 45回 | スキャン: 100000件 | 結果: 30000件
時間: 1593ms | RCU: 5686.5 | ページ: 45回 | スキャン: 100000件 | 結果: 30000件
【Query (GSI)】
時間: 1409ms | RCU: 1727 | ページ: 15回 | スキャン: 30000件 | 結果: 30000件
時間: 1352ms | RCU: 1727 | ページ: 15回 | スキャン: 30000件 | 結果: 30000件
時間: 1020ms | RCU: 1727 | ページ: 15回 | スキャン: 30000件 | 結果: 30000件
時間: 991ms | RCU: 1727 | ページ: 15回 | スキャン: 30000件 | 結果: 30000件
時間: 933ms | RCU: 1727 | ページ: 15回 | スキャン: 30000件 | 結果: 30000件
時間: 1222ms | RCU: 1727 | ページ: 15回 | スキャン: 30000件 | 結果: 30000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 100000 件 / レコードサイズ: 1KB(テーブル: Products-100000-1kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 3173ms | RCU: 11968.5 | ページ: 94回 | スキャン: 100000件 | 結果: 30000件
時間: 2797ms | RCU: 11968.5 | ページ: 94回 | スキャン: 100000件 | 結果: 30000件
時間: 2540ms | RCU: 11968.5 | ページ: 94回 | スキャン: 100000件 | 結果: 30000件
時間: 2434ms | RCU: 11968.5 | ページ: 94回 | スキャン: 100000件 | 結果: 30000件
時間: 2345ms | RCU: 11968.5 | ページ: 94回 | スキャン: 100000件 | 結果: 30000件
時間: 2326ms | RCU: 11968.5 | ページ: 94回 | スキャン: 100000件 | 結果: 30000件
【Query (GSI)】
時間: 1878ms | RCU: 3611.5 | ページ: 29回 | スキャン: 30000件 | 結果: 30000件
時間: 1808ms | RCU: 3611.5 | ページ: 29回 | スキャン: 30000件 | 結果: 30000件
時間: 1761ms | RCU: 3611.5 | ページ: 29回 | スキャン: 30000件 | 結果: 30000件
時間: 1845ms | RCU: 3611.5 | ページ: 29回 | スキャン: 30000件 | 結果: 30000件
時間: 1739ms | RCU: 3611.5 | ページ: 29回 | スキャン: 30000件 | 結果: 30000件
時間: 1630ms | RCU: 3611.5 | ページ: 29回 | スキャン: 30000件 | 結果: 30000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 100000 件 / レコードサイズ: 5KB(テーブル: Products-100000-5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 13468ms | RCU: 62078.5 | ページ: 484回 | スキャン: 100000件 | 結果: 30000件
時間: 12190ms | RCU: 62078.5 | ページ: 484回 | スキャン: 100000件 | 結果: 30000件
時間: 10703ms | RCU: 62078.5 | ページ: 484回 | スキャン: 100000件 | 結果: 30000件
時間: 9060ms | RCU: 62078.5 | ページ: 484回 | スキャン: 100000件 | 結果: 30000件
時間: 10093ms | RCU: 62078.5 | ページ: 484回 | スキャン: 100000件 | 結果: 30000件
時間: 10428ms | RCU: 62078.5 | ページ: 484回 | スキャン: 100000件 | 結果: 30000件
【Query (GSI)】
時間: 6018ms | RCU: 18625.5 | ページ: 148回 | スキャン: 30000件 | 結果: 30000件
時間: 5457ms | RCU: 18625.5 | ページ: 148回 | スキャン: 30000件 | 結果: 30000件
時間: 5131ms | RCU: 18625.5 | ページ: 148回 | スキャン: 30000件 | 結果: 30000件
時間: 5228ms | RCU: 18625.5 | ページ: 148回 | スキャン: 30000件 | 結果: 30000件
時間: 5120ms | RCU: 18625.5 | ページ: 148回 | スキャン: 30000件 | 結果: 30000件
時間: 4898ms | RCU: 18625.5 | ページ: 148回 | スキャン: 30000件 | 結果: 30000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 1000000 件 / レコードサイズ: 0.5KB(テーブル: Products-1000000-0.5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 17739ms | RCU: 56737.5 | ページ: 442回 | スキャン: 1000000件 | 結果: 300000件
時間: 16949ms | RCU: 56737.5 | ページ: 442回 | スキャン: 1000000件 | 結果: 300000件
時間: 16245ms | RCU: 56737.5 | ページ: 442回 | スキャン: 1000000件 | 結果: 300000件
時間: 15762ms | RCU: 56737.5 | ページ: 442回 | スキャン: 1000000件 | 結果: 300000件
時間: 15658ms | RCU: 56737.5 | ページ: 442回 | スキャン: 1000000件 | 結果: 300000件
時間: 14973ms | RCU: 56737.5 | ページ: 442回 | スキャン: 1000000件 | 結果: 300000件
【Query (GSI)】
時間: 11090ms | RCU: 17234 | ページ: 142回 | スキャン: 300000件 | 結果: 300000件
時間: 10218ms | RCU: 17234 | ページ: 142回 | スキャン: 300000件 | 結果: 300000件
時間: 10346ms | RCU: 17234 | ページ: 142回 | スキャン: 300000件 | 結果: 300000件
時間: 9752ms | RCU: 17234 | ページ: 142回 | スキャン: 300000件 | 結果: 300000件
時間: 8952ms | RCU: 17234 | ページ: 142回 | スキャン: 300000件 | 結果: 300000件
時間: 9975ms | RCU: 17234 | ページ: 142回 | スキャン: 300000件 | 結果: 300000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 1000000 件 / レコードサイズ: 1KB(テーブル: Products-1000000-1kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 30423ms | RCU: 119553.5 | ページ: 931回 | スキャン: 1000000件 | 結果: 300000件
時間: 27024ms | RCU: 119553.5 | ページ: 931回 | スキャン: 1000000件 | 結果: 300000件
時間: 26513ms | RCU: 119553.5 | ページ: 931回 | スキャン: 1000000件 | 結果: 300000件
時間: 26440ms | RCU: 119553.5 | ページ: 931回 | スキャン: 1000000件 | 結果: 300000件
時間: 25164ms | RCU: 119553.5 | ページ: 931回 | スキャン: 1000000件 | 結果: 300000件
時間: 26496ms | RCU: 119553.5 | ページ: 931回 | スキャン: 1000000件 | 結果: 300000件
【Query (GSI)】
時間: 21304ms | RCU: 36094 | ページ: 288回 | スキャン: 300000件 | 結果: 300000件
時間: 19689ms | RCU: 36094 | ページ: 288回 | スキャン: 300000件 | 結果: 300000件
時間: 19486ms | RCU: 36094 | ページ: 288回 | スキャン: 300000件 | 結果: 300000件
時間: 18363ms | RCU: 36094 | ページ: 288回 | スキャン: 300000件 | 結果: 300000件
時間: 18237ms | RCU: 36094 | ページ: 288回 | スキャン: 300000件 | 結果: 300000件
時間: 17768ms | RCU: 36094 | ページ: 288回 | スキャン: 300000件 | 結果: 300000件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 データ件数: 1000000 件 / レコードサイズ: 5KB(テーブル: Products-1000000-5kb)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【Scan + FilterExpression】
時間: 142697ms | RCU: 620773 | ページ: 4831回 | スキャン: 1000000件 | 結果: 300000件
時間: 138911ms | RCU: 620773 | ページ: 4831回 | スキャン: 1000000件 | 結果: 300000件
時間: 137300ms | RCU: 620773 | ページ: 4831回 | スキャン: 1000000件 | 結果: 300000件
時間: 136916ms | RCU: 620773 | ページ: 4831回 | スキャン: 1000000件 | 結果: 300000件
時間: 136718ms | RCU: 620773 | ページ: 4831回 | スキャン: 1000000件 | 結果: 300000件
時間: 137025ms | RCU: 620773 | ページ: 4831回 | スキャン: 1000000件 | 結果: 300000件
【Query (GSI)】
時間: 62520ms | RCU: 186162.5 | ページ: 1479回 | スキャン: 300000件 | 結果: 300000件
時間: 56798ms | RCU: 186162.5 | ページ: 1479回 | スキャン: 300000件 | 結果: 300000件
時間: 55091ms | RCU: 186162.5 | ページ: 1479回 | スキャン: 300000件 | 結果: 300000件
時間: 54324ms | RCU: 186162.5 | ページ: 1479回 | スキャン: 300000件 | 結果: 300000件
時間: 52652ms | RCU: 186162.5 | ページ: 1479回 | スキャン: 300000件 | 結果: 300000件
時間: 51567ms | RCU: 186162.5 | ページ: 1479回 | スキャン: 300000件 | 結果: 300000件
=========================================
RCU消費量
RCUに約3.3倍の差が出たのは、「フィルタ効率」 の違いが理由です。
今回は「対象データが全体の約30%」という構成だったため、全件を読み取るScanは、Queryに対して約3倍のコストがかかりました。
ヒット率(対象データの割合)が下がるほど、このコスト差はさらに広がることになります。
DynamoDB Standard テーブルクラス(東京リージョン)
| オンデマンドスループットタイプ | 料金 |
|---|---|
| 書き込み要求単位 (WRU) | 書き込み要求ユニット 100 万あたり USD 0.715 |
| 読み出し要求単位 (RRU) | 読み出し要求ユニット 100 万あたり USD 0.1425 |
※料金は2026年2月現在のものです
まとめ
今回の検証を通して、DynamoDB における「Scanは避けるべき」 という定説に対して、データ量/データサイズという2つの軸で具体的な境界線を知ることができました。
結論
10万件を超えたあたりから、 UX に影響が出始め、Query (GSI) でも1〜5秒、Scan では 2〜13秒の遅延が発生する。
100万規模かつ、5KBレコードとなると Scan に 2分以上 かかってくるため、同期的なAPIレスポンスとしては現実的ではない。
設計判断の指針
| データ規模 | 判断 |
|---|---|
| 〜1,000件 | Scanで十分。GSIの管理コストを避けられる |
| 1万〜10万件 | GSI推奨。時間差・RCU差が顕著になり始める |
| 10万件〜 | GSI必須。Scanでは実用上、許容できないほどの遅延が発生 |
「Scan = 絶対悪」と反射的に避けるのではなく、将来のデータ件数とレコードサイズを見据えた設計判断が大切です。
全体のコード








