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