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

弊社では、API Gateway + Lambda + DynamoDBを利用したサーバーレスアーキテクチャでの開発を多く行っています。
その中で、度々パフォーマンス問題が発生する事がありますが、特にDynamoDBのScan操作に起因するケースが目立ちます。
この記事では、ScanとQueryのパフォーマンスとコストの違いについて、実際のデータを用いた検証結果をもとに解説します。
事前準備
DynamoDBに10万件のサンプルデータを投入し、ScanとQueryの動作を比較します。
データ準備手順
-
CSVからデータ投入:
AWS公式サンプルリポジトリ aws-samples/csv-to-dynamodb を利用。
投入手順は こちらの記事 を参照。
-
データ件数確認: 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 |
考察
- パフォーマンス: QueryはScanよりも4倍以上高速。指定したパーティションキーに基づいて効率的にデータを取得できるためです。
- コスト効率: 消費キャパシティユニットもScanの約4分の1。大量データの操作では大幅なコスト削減が見込めます。
- 使用ケース: データを効率的に取得したい場合や条件検索が必要な場合はQueryを使用すべき。一方、条件に合致するパーティションキーがない場合や全件検索が必要な場合に限り、Scanを検討します。
まとめ
データ件数10万件の実験結果から、QueryはScanよりも圧倒的に高速かつ低コストであることが確認できました。
結論:
- DynamoDBのパフォーマンス最適化を考えるなら、Queryを優先的に使用。
- 特に、必要なデータに応じたパーティションキーやGSI設計を行うことが重要です。
これからDynamoDBを使った開発を行う方は、この結果を参考に、効率的なデータ操作を実現してください。
この記事をシェアする
合同会社raisexでは一緒に働く仲間を募集中です。
ご興味のある方は以下の採用情報をご確認ください。