DynamoDBのScan vs Query(GSI)を100万件まで検証して、性能に差が出るラインを調べてみた

DynamoDBのScan vs Query(GSI)を100万件まで検証して、性能に差が出るラインを調べてみた

2026.02.09

人材育成室 育成メンバーチームで 研修中の はす です。

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個用意します。

スクリーンショット 2026-02-05 9.08.03

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 用で用意します。

スクリーンショット 2026-02-05 9.09.36

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月現在のものです

https://aws.amazon.com/jp/dynamodb/pricing/on-demand/

まとめ

今回の検証を通して、DynamoDB における「Scanは避けるべき」 という定説に対して、データ量/データサイズという2つの軸で具体的な境界線を知ることができました。

結論
10万件を超えたあたりから、 UX に影響が出始め、Query (GSI) でも1〜5秒、Scan では 2〜13秒の遅延が発生する。
100万規模かつ、5KBレコードとなると Scan2分以上 かかってくるため、同期的なAPIレスポンスとしては現実的ではない。

設計判断の指針

データ規模 判断
〜1,000件 Scanで十分。GSIの管理コストを避けられる
1万〜10万件 GSI推奨。時間差・RCU差が顕著になり始める
10万件〜 GSI必須。Scanでは実用上、許容できないほどの遅延が発生

「Scan = 絶対悪」と反射的に避けるのではなく、将来のデータ件数とレコードサイズを見据えた設計判断が大切です。

全体のコード

https://github.com/HasutoSasaki/dynamodb-scan-vs-query-benchmark

この記事をシェアする

FacebookHatena blogX

関連記事