DynamoDBでのソート・フィルタ取得における `query` と `scan` の選択
背景
あるiOSアプリのバックエンドAPIを AWS Lambda + DynamoDB 構成で開発していた際、
「投稿一覧を取得する」APIの実装に取り組みました。
このAPIでは、以下のような要件を満たす必要がありました:
- 論理削除されていない投稿のみを対象とする(
deleted != true) - 投稿の作成日時(
created_at)の降順でソートする - ページングに対応させたい(
limit、lastKey)
初期実装では、テーブル内の全件を走査する scan 関数を使用し、
Lambda関数内で deleted をフィルタし、Python側で created_at を降順ソートする構成としました。
一見シンプルに見えるこの実装ですが、投稿数が増加していくことを考慮すると、
パフォーマンス面に不安が残る構成でした。
そこで色々と調べたので備忘として書き残します。
scan と query の違い
DynamoDBでデータを取得するには、scan と query の2種類の方法があります。
この2つは、見た目は似ていますが内部動作が大きく異なります。
| 処理 | 説明 | パフォーマンス |
|---|---|---|
scan |
テーブル全体を走査し、あとから条件でフィルタ | 遅い(全件分の処理が必要) |
query |
インデックスを用いて、キー条件に合致するデータだけを取得 | 速い(必要なデータだけ取得) |
たとえば、10,000件の投稿がある場合、scan はそのすべてを一度取得したうえで、Lambda関数側で整形・ソートを行います。
この場合、ネットワーク転送・メモリ使用量・Lambdaの実行時間がすべて増加します。
一方、query は 特定のキー(Partition KeyまたはGSI)を指定してピンポイントでデータを取得できるため、
DynamoDB内部で最適化された形で取得でき、格段に速く・安くなります。
降順・フィルタ条件がある場合の設計方針
今回の要件に沿って scan を使用した場合の流れは、以下のとおりです。
- DynamoDBの全項目を
scan()で取得 -
FilterExpressionでdeleted != trueを除外 - Lambda関数内で
created_atを降順ソート
これは構文上はわかりやすいですが、スケーラビリティに難があります。
これに対し、あらかじめ以下のような GSI(Global Secondary Index) を設計した上で query を使えば、
同様の要件を効率的に実現できます。
- GSIのパーティションキー:
type(例:public) - GSIのソートキー:
created_at -
FilterExpression:deleted != true -
ScanIndexForward = Falseにより降順取得
response = table.query(
IndexName='created_at-index',
KeyConditionExpression=Key('type').eq('public'),
FilterExpression=Attr('deleted').ne(True),
Limit=10,
ScanIndexForward=False
)
このようにすることで、DynamoDB上で絞り込みとソートの大部分を完結でき、Lambda側の処理はシンプルかつ高速になります。
最後に
Lambda + DynamoDB構成において、「件数の多いリソースを、条件付き・ソート付きで取得する」といったケースでは、
query を使うためのインデックス設計を前提にするべきです。
開発初期段階では scan を使って素早く動作確認するのも一つの手ですが、
プロダクションや将来的なスケーラビリティを見据えるならば、早期に query ベースの設計へ移行することが望ましいです。
Discussion