🦔

DynamoDBでのソート・フィルタ取得における `query` と `scan` の選択

に公開

背景

あるiOSアプリのバックエンドAPIを AWS Lambda + DynamoDB 構成で開発していた際、
「投稿一覧を取得する」APIの実装に取り組みました。

このAPIでは、以下のような要件を満たす必要がありました:

  • 論理削除されていない投稿のみを対象とする(deleted != true
  • 投稿の作成日時(created_at)の降順でソートする
  • ページングに対応させたい(limitlastKey

初期実装では、テーブル内の全件を走査する scan 関数を使用し、
Lambda関数内で deleted をフィルタし、Python側で created_at を降順ソートする構成としました。

一見シンプルに見えるこの実装ですが、投稿数が増加していくことを考慮すると、
パフォーマンス面に不安が残る構成でした。
そこで色々と調べたので備忘として書き残します。

scanquery の違い

DynamoDBでデータを取得するには、scanquery の2種類の方法があります。
この2つは、見た目は似ていますが内部動作が大きく異なります。

処理 説明 パフォーマンス
scan テーブル全体を走査し、あとから条件でフィルタ 遅い(全件分の処理が必要)
query インデックスを用いて、キー条件に合致するデータだけを取得 速い(必要なデータだけ取得)

たとえば、10,000件の投稿がある場合、scan はそのすべてを一度取得したうえで、Lambda関数側で整形・ソートを行います。
この場合、ネットワーク転送・メモリ使用量・Lambdaの実行時間がすべて増加します

一方、query特定のキー(Partition KeyまたはGSI)を指定してピンポイントでデータを取得できるため、
DynamoDB内部で最適化された形で取得でき、格段に速く・安くなります


降順・フィルタ条件がある場合の設計方針

今回の要件に沿って scan を使用した場合の流れは、以下のとおりです。

  1. DynamoDBの全項目を scan() で取得
  2. FilterExpressiondeleted != true を除外
  3. Lambda関数内で created_at を降順ソート

これは構文上はわかりやすいですが、スケーラビリティに難があります

これに対し、あらかじめ以下のような GSI(Global Secondary Index) を設計した上で query を使えば、
同様の要件を効率的に実現できます。

  • GSIのパーティションキー:type(例:public
  • GSIのソートキー:created_at
  • FilterExpressiondeleted != 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