DynamoDB Scan vs Query: パフォーマンスとコストの比較

投稿更新日: 2025/6/5

サムネイル

弊社では、API Gateway + Lambda + DynamoDBを利用したサーバーレスアーキテクチャでの開発を多く行っています。

その中で、度々パフォーマンス問題が発生する事がありますが、特にDynamoDBのScan操作に起因するケースが目立ちます。

この記事では、ScanQueryのパフォーマンスとコストの違いについて、実際のデータを用いた検証結果をもとに解説します。


事前準備

DynamoDBに10万件のサンプルデータを投入し、ScanとQueryの動作を比較します。

データ準備手順

  1. CSVからデータ投入:

    AWS公式サンプルリポジトリ aws-samples/csv-to-dynamodb を利用。

    投入手順は こちらの記事 を参照。

  2. データ件数確認: DynamoDB CLIを用いてデータ件数を確認します。

    aws dynamodb scan --table-name test_datas --select COUNT
    

    結果:

    {
        "Count": 100000,
        "ScannedCount": 100000,
        "ConsumedCapacity": null
    }

    10万件のデータが投入されました。


実験1: Scan

実装コード

以下のLambda関数を使用してDynamoDBのScan操作を実行します。

import boto3
from boto3.dynamodb.conditions import Attr

def lambda_handler(event, context):
    table_name = 'test_datas'
    region = 'us-east-1'

    dynamodb = boto3.resource('dynamodb', region_name=region)
    table = dynamodb.Table(table_name)

    filter_expression = Attr('Country').eq('Australia')

    try:
        options = {
            'FilterExpression': filter_expression,
            'Limit': 1000,
            'ReturnConsumedCapacity': 'TOTAL'
        }
        while True:
            response = table.scan(**options)
            print(response['Items'])
            print(response['ConsumedCapacity'])
            next_token = response.get('LastEvaluatedKey', None)
            print(next_token)

            if next_token:
                options['ExclusiveStartKey'] = next_token
            else:
                break

        return {'statusCode': 200}
    except Exception as e:
        print('エラー:', e)
        return {'statusCode': 500, 'body': 'サーバーエラーが発生しました'}

実行結果

ウォームスタートでの実行結果:

  • 実行時間: 約5秒
  • 消費キャパシティユニット: 62

ログの一部:

Function Logs:
'3/30/15', 'Region': 'Australia and Oceania', 'TotalRevenue': '58517.76', 'ShipDate': '5/8/15', 'SalesChannel': 'Offline', 'UnitCost': '6.92', 'uuid': '757149684', 'TotalCost': '43402.24'}, {'ItemType': 'Snacks', 'TotalProfit': '206885.28', 'UnitsSold': '3752', 'UnitPrice': '152.58', 'Country': 'Australia', 'OrderPriority': 'C', 'OrderDate': '4/19/10', 'Region': 'Australia and Oceania', 'TotalRevenue': '572480.16', 'ShipDate': '6/5/10', 'SalesChannel': 'Offline', 'UnitCost': '97.44', 'uuid': '340443837', 'TotalCost': '365594.88'}, {'ItemType': 'Snacks', 'TotalProfit': '313912.02', 'UnitsSold': '5693', 'UnitPrice': '152.58', 'Country': 'Australia', 'OrderPriority': 'M', 'OrderDate': '9/12/12', 'Region': 'Australia and Oceania', 'TotalRevenue': '868637.94', 'ShipDate': '10/27/12', 'SalesChannel': 'Online', 'UnitCost': '97.44', 'uuid': '892960441', 'TotalCost': '554725.92'}, {'ItemType': 'Meat', 'TotalProfit': '462061.6', 'UnitsSold': '8078', 'UnitPrice': '421.89', 'Country': 'Australia', 'OrderPriority': 'M', 'OrderDate': '8/23/13', 'Region': 'Australia and Oceania', 'TotalRevenue': '3408027.42', 'ShipDate': '8/27/13', 'SalesChannel': 'Offline', 'UnitCost': '364.69', 'uuid': '543180644', 'TotalCost': '2945965.82'}, {'ItemType': 'Beverages', 'TotalProfit': '152559.72', 'UnitsSold': '9742', 'UnitPrice': '47.45', 'Country': 'Australia', 'OrderPriority': 'L', 'OrderDate': '1/27/16', 'Region': 'Australia and Oceania', 'TotalRevenue': '462257.9', 'ShipDate': '2/14/16', 'SalesChannel': 'Offline', 'UnitCost': '31.79', 'uuid': '671733558', 'TotalCost': '309698.18'}]
{'TableName': 'test_datas', 'CapacityUnits': 28.5}
{'uuid': '147039830'}
[{'ItemType': 'Cosmetics', 'TotalProfit': '919076.82', 'UnitsSold': '5286', 'UnitPrice': '437.2', 'Country': 'Australia', 'OrderPriority': 'M', 'OrderDate': '5/12/14', 'Region': 'Australia and Oceania', 'TotalRevenue': '2311039.2', 'ShipDate': '5/31/14', 'SalesChannel': 'Online', 'UnitCost': '263.33', 'uuid': '746805799', 'TotalCost': '1391962.38'}, {'ItemType': 'Vegetables', 'TotalProfit': '273731.68', 'UnitsSold': '4336', 'UnitPrice': '154.06', 'Country': 'Australia', 'OrderPriority': 'L', 'OrderDate': '11/20/15', 'Region': 'Australia and Oceania', 'TotalRevenue': '668004.16', 'ShipDate': '12/21/15', 'SalesChannel': 'Online', 'UnitCost': '90.93', 'uuid': '964156997', 'TotalCost': '394272.48'}, {'ItemType': 'Cosmetics', 'TotalProfit': '734253.01', 'UnitsSold': '4223', 'UnitPrice': '437.2', 'Country': 'Australia', 'OrderPriority': 'C', 'OrderDate': '5/30/10', 'Region': 'Australia and Oceania', 'TotalRevenue': '1846295.6', 'ShipDate': '7/12/10', 'SalesChannel': 'Offline', 'UnitCost': '263.33', 'uuid': '182464730', 'TotalCost': '1112042.59'}, {'ItemType': 'Cosmetics', 'TotalProfit': '398683.91', 'UnitsSold': '2293', 'UnitPrice': '437.2', 'Country': 'Australia', 'OrderPriority': 'C', 'OrderDate': '5/24/15', 'Region': 'Australia and Oceania', 'TotalRevenue': '1002499.6', 'ShipDate': '5/25/15', 'SalesChannel': 'Offline', 'UnitCost': '263.33', 'uuid': '841381347', 'TotalCost': '603815.69'}, {'ItemType': 'Household', 'TotalProfit': '251743.87', 'UnitsSold': '1519', 'UnitPrice': '668.27', 'Country': 'Australia', 'OrderPriority': 'C', 'OrderDate': '1/3/14', 'Region': 'Australia and Oceania', 'TotalRevenue': '1015102.13', 'ShipDate': '1/25/14', 'SalesChannel': 'Online', 'UnitCost': '502.54', 'uuid': '351520287', 'TotalCost': '763358.26'}, {'ItemType': 'Household', 'TotalProfit': '1447485.82', 'UnitsSold': '8734', 'UnitPrice': '668.27', 'Country': 'Australia', 'OrderPriority': 'C', 'OrderDate': '1/29/14', 'Region': 'Australia and Oceania', 'TotalRevenue': '5836670.18', 'ShipDate': '2/2/14', 'SalesChannel': 'Offline', 'UnitCost': '502.54', 'uuid': '391825520', 'TotalCost': '4389184.36'}]
{'TableName': 'test_datas', 'CapacityUnits': 28.5}
{'uuid': '690726172'}
[]
{'TableName': 'test_datas', 'CapacityUnits': 0.5}
None
END RequestId: f6e29395-105a-4388-ae4b-a15905769f51
REPORT RequestId: f6e29395-105a-4388-ae4b-a15905769f51	Duration: 4904.22 ms	Billed Duration: 4905 ms	Memory Size: 128 MB	Max Memory Used: 77 MB

実験2: Query

GSI(グローバルセカンダリインデックス)作成

Country を条件にQueryを実行するため、以下の条件でGSIを作成しました。

  • GSI名: Country-index
  • パーティションキー: Country(文字列)

実装コード

以下のLambda関数を使用してDynamoDBのQuery操作を実行します。

import boto3
from boto3.dynamodb.conditions import Key

def lambda_handler(event, context):
    table_name = 'test_datas'
    region = 'us-east-1'

    dynamodb = boto3.resource('dynamodb', region_name=region)
    table = dynamodb.Table(table_name)

    key_condition_expression = Key('Country').eq('Australia')

    try:
        options = {
            'KeyConditionExpression': key_condition_expression,
            'Limit': 1000,
            'ReturnConsumedCapacity': 'TOTAL'
        }
        while True:
            response = table.query(**options)
            print(response['Items'])
            print(response['ConsumedCapacity'])
            next_token = response.get('LastEvaluatedKey', None)
            print(next_token)

            if next_token:
                options['ExclusiveStartKey'] = next_token
            else:
                break

        return {'statusCode': 200}
    except Exception as e:
        print('エラー:', e)
        return {'statusCode': 500, 'body': 'サーバーエラーが発生しました'}

実行結果

ウォームスタートでの実行結果:

  • 実行時間: 約1.1秒
  • 消費キャパシティユニット: 16.5

ログの一部:

Function Logs:
ItemType': 'Fruits', 'TotalProfit': '15115.52', 'UnitsSold': '6272', 'Country': 'Australia', 'UnitPrice': '9.33', 'OrderPriority': 'H', 'OrderDate': '3/30/15', 'Region': 'Australia and Oceania', 'TotalRevenue': '58517.76', 'ShipDate': '5/8/15', 'SalesChannel': 'Offline', 'UnitCost': '6.92', 'uuid': '757149684', 'TotalCost': '43402.24'}, {'ItemType': 'Snacks', 'TotalProfit': '206885.28', 'UnitsSold': '3752', 'Country': 'Australia', 'UnitPrice': '152.58', 'OrderPriority': 'C', 'OrderDate': '4/19/10', 'Region': 'Australia and Oceania', 'TotalRevenue': '572480.16', 'ShipDate': '6/5/10', 'SalesChannel': 'Offline', 'UnitCost': '97.44', 'uuid': '340443837', 'TotalCost': '365594.88'}, {'ItemType': 'Snacks', 'TotalProfit': '313912.02', 'UnitsSold': '5693', 'Country': 'Australia', 'UnitPrice': '152.58', 'OrderPriority': 'M', 'OrderDate': '9/12/12', 'Region': 'Australia and Oceania', 'TotalRevenue': '868637.94', 'ShipDate': '10/27/12', 'SalesChannel': 'Online', 'UnitCost': '97.44', 'uuid': '892960441', 'TotalCost': '554725.92'}, {'ItemType': 'Meat', 'TotalProfit': '462061.6', 'UnitsSold': '8078', 'Country': 'Australia', 'UnitPrice': '421.89', 'OrderPriority': 'M', 'OrderDate': '8/23/13', 'Region': 'Australia and Oceania', 'TotalRevenue': '3408027.42', 'ShipDate': '8/27/13', 'SalesChannel': 'Offline', 'UnitCost': '364.69', 'uuid': '543180644', 'TotalCost': '2945965.82'}, {'ItemType': 'Beverages', 'TotalProfit': '152559.72', 'UnitsSold': '9742', 'Country': 'Australia', 'UnitPrice': '47.45', 'OrderPriority': 'L', 'OrderDate': '1/27/16', 'Region': 'Australia and Oceania', 'TotalRevenue': '462257.9', 'ShipDate': '2/14/16', 'SalesChannel': 'Offline', 'UnitCost': '31.79', 'uuid': '671733558', 'TotalCost': '309698.18'}, {'ItemType': 'Cosmetics', 'TotalProfit': '919076.82', 'UnitsSold': '5286', 'Country': 'Australia', 'UnitPrice': '437.2', 'OrderPriority': 'M', 'OrderDate': '5/12/14', 'Region': 'Australia and Oceania', 'TotalRevenue': '2311039.2', 'ShipDate': '5/31/14', 'SalesChannel': 'Online', 'UnitCost': '263.33', 'uuid': '746805799', 'TotalCost': '1391962.38'}, {'ItemType': 'Vegetables', 'TotalProfit': '273731.68', 'UnitsSold': '4336', 'Country': 'Australia', 'UnitPrice': '154.06', 'OrderPriority': 'L', 'OrderDate': '11/20/15', 'Region': 'Australia and Oceania', 'TotalRevenue': '668004.16', 'ShipDate': '12/21/15', 'SalesChannel': 'Online', 'UnitCost': '90.93', 'uuid': '964156997', 'TotalCost': '394272.48'}, {'ItemType': 'Cosmetics', 'TotalProfit': '734253.01', 'UnitsSold': '4223', 'Country': 'Australia', 'UnitPrice': '437.2', 'OrderPriority': 'C', 'OrderDate': '5/30/10', 'Region': 'Australia and Oceania', 'TotalRevenue': '1846295.6', 'ShipDate': '7/12/10', 'SalesChannel': 'Offline', 'UnitCost': '263.33', 'uuid': '182464730', 'TotalCost': '1112042.59'}, {'ItemType': 'Cosmetics', 'TotalProfit': '398683.91', 'UnitsSold': '2293', 'Country': 'Australia', 'UnitPrice': '437.2', 'OrderPriority': 'C', 'OrderDate': '5/24/15', 'Region': 'Australia and Oceania', 'TotalRevenue': '1002499.6', 'ShipDate': '5/25/15', 'SalesChannel': 'Offline', 'UnitCost': '263.33', 'uuid': '841381347', 'TotalCost': '603815.69'}, {'ItemType': 'Household', 'TotalProfit': '251743.87', 'UnitsSold': '1519', 'Country': 'Australia', 'UnitPrice': '668.27', 'OrderPriority': 'C', 'OrderDate': '1/3/14', 'Region': 'Australia and Oceania', 'TotalRevenue': '1015102.13', 'ShipDate': '1/25/14', 'SalesChannel': 'Online', 'UnitCost': '502.54', 'uuid': '351520287', 'TotalCost': '763358.26'}, {'ItemType': 'Household', 'TotalProfit': '1447485.82', 'UnitsSold': '8734', 'Country': 'Australia', 'UnitPrice': '668.27', 'OrderPriority': 'C', 'OrderDate': '1/29/14', 'Region': 'Australia and Oceania', 'TotalRevenue': '5836670.18', 'ShipDate': '2/2/14', 'SalesChannel': 'Offline', 'UnitCost': '502.54', 'uuid': '391825520', 'TotalCost': '4389184.36'}]
{'TableName': 'test_datas', 'CapacityUnits': 16.5}
None
END RequestId: 902e8513-28db-4e25-9a4e-df9cba5f9410
REPORT RequestId: 902e8513-28db-4e25-9a4e-df9cba5f9410	Duration: 1158.68 ms	Billed Duration: 1159 ms	Memory Size: 128 MB	Max Memory Used: 82 MB

比較結果

操作方法実行時間消費キャパシティユニット
Scan約5秒62
Query約1.1秒16.5

考察

  1. パフォーマンス: QueryはScanよりも4倍以上高速。指定したパーティションキーに基づいて効率的にデータを取得できるためです。
  2. コスト効率: 消費キャパシティユニットもScanの約4分の1。大量データの操作では大幅なコスト削減が見込めます。
  3. 使用ケース: データを効率的に取得したい場合や条件検索が必要な場合はQueryを使用すべき。一方、条件に合致するパーティションキーがない場合や全件検索が必要な場合に限り、Scanを検討します。

まとめ

データ件数10万件の実験結果から、QueryScanよりも圧倒的に高速かつ低コストであることが確認できました。

結論:

  • DynamoDBのパフォーマンス最適化を考えるなら、Queryを優先的に使用
  • 特に、必要なデータに応じたパーティションキーやGSI設計を行うことが重要です。

これからDynamoDBを使った開発を行う方は、この結果を参考に、効率的なデータ操作を実現してください。


この記事をシェアする

合同会社raisexでは一緒に働く仲間を募集中です。

ご興味のある方は以下の採用情報をご確認ください。