DynamoDB の Query と Scan:どちらをいつ使うか(例付き)
Query はパーティションキーで単一のアイテムコレクションを読み取り(オプションで
ソートキーで絞り込み)、Scan は テーブル全体 を読んでから後でフィルタします。
API では似て見えますが、課金とスケールはまったく異なります。
DynamoDB で Query と Scan をいつ使い分けるべきか?
必要なパーティションを名指しできる場合はいつでも Query を使いましょう — 1つのアイテムコレクションを読み取り、マッチしたアイテム分だけ課金されます。Scan は1回限りのエクスポートや小さなテーブルにのみ使用してください。Scan はすべてのアイテムを読み取り、FilterExpression が実行される前にテーブル全体を課金します。実際のデータでは、Query が勝ります。
- Query は対象を絞ります。マッチしたパーティション内のアイテムに対して支払います。
- Scan は網羅的です。すべてのアイテムを読むために支払い、その後ほとんどを
FilterExpressionで捨てます。フィルタは読み取りが計測された 後 に動きます。
ある程度の規模のテーブルでは、フィルタ付きの Scan は典型的な「なぜ請求額が巨大で、
レイテンシが RDS より悪いのか」という落とし穴です。
並べて比較
| Query | Scan | |
|---|---|---|
| 読み取り | 1つのパーティション(PK で) | テーブル内の すべての アイテム |
| 課金キャパシティ | パーティション内でマッチしたアイテム | フィルタ前の テーブル全体 |
FilterExpression | 読み取り後に適用 — それでも読み取り分は課金される | 同様 — フィルタはコストを削らない |
| レイテンシ | テーブルが成長しても一定 | テーブルサイズとともに増加 |
| ページネーション | 1 MB/ページ → LastEvaluatedKey | 1 MB/ページ。並列化可能 |
| 用途 | 既知のアクセスパターン | 1回限りのエクスポート、小さな設定テーブル |
FilterExpression は、両方の操作で、DynamoDB が読み取りを計測した 後 に
動きます。「10行を返す」Scan が、100万行の読み取り分を課金することがあります —
フィルタは便宜であって、決してコスト管理ではありません。
フル Scan の実際のコスト
数字で見てみましょう。DynamoDB は読み取りを 4 KB 単位で計測します。
強い整合性のある 読み取りは 4 KB ごとに
1 読み取りキャパシティユニット を消費し、
結果整合性のある 読み取りはその半分です。Query と
Scan は、返したアイテムではなく 触れた すべてのアイテムのサイズを合計し、
次の 4 KB に切り上げます。
1アイテム平均 2 KB の 100万アイテムのテーブル(データ約 2 GB)と、そのうち 10 アイテムを必要とするアクセスパターンを考えてみましょう:
| 読み取りアイテム数 | 計測データ量 | 読み取りユニット(結果整合性) | |
|---|---|---|---|
Scan + FilterExpression | 1,000,000 | 約 2 GB | 約 262,000 |
マッチするキーでの Query | 10 | 20 KB | 3 |
同じ 10 アイテムで、5桁もの差です — しかも Scan は、フィルタが 10 アイテムに
マッチしても 1件もマッチしなくても、実行のたびにその約 262,000 を課金します。
オンデマンド 課金では、それはそのままリクエストユニットとして
請求に乗ります。プロビジョンド テーブルでは、大きな Scan が
本番トラフィックとスループットを奪い合い、スロットリングして
ProvisionedThroughputExceededException
を引き起こすことがあります。
さらに、人を驚かせるコストの事実を3つ:
Select: COUNTはタダではありません。 件数を数える Query や Scan は、 アイテムを読み取るのとまったく同じ読み取りキャパシティを消費します — ただ返さないだけです。Limitが制限するのは 評価された アイテム数で、マッチしたアイテム数では ありません。 フィルタと組み合わせると、ページが空で返ってきても、 まるまる 1ページ分の読み取りが課金されることがあります。- 推測する必要はありません。
ReturnConsumedCapacity: TOTALを渡せば、 すべてのレスポンスが直前に消費したキャパシティを報告します。
自分のアイテムの重さは アイテムサイズ計算ツール で確認し、 読み取りユニットを月額の料金に換算するには 料金計算ツール を使ってください。
Query を使う
Query PK = "USER#42" AND SK begins_with "ORDER#"
よくあるアクセスパターンに答えるために Scan に手が伸びるようなら、それはモデリングの
サインです。グローバルセカンダリインデックス を追加して、その
パターンを Query にしてください。
選択は1つの問いに帰着します — 必要なパーティションを名指しできますか?
キーがわかっていれば Query し、わからなければ GSI を追加して Query にできるようにし、
どのキーも当てはまらないときだけ Scan にフォールバックします。
Scan で問題ないとき
1回限りのエクスポート、小さな設定テーブル、そしてテーブル全体を意図的にページ送り
するバックグラウンドジョブです。本当にすべてを読まなければならないときは、
Segment/TotalSegments を使って Scan をワーカー間で分割し
(並列スキャン —
DynamoDB の並列スキャン を参照)、
LastEvaluatedKey で正しくページ送りしてください
(ページネーションガイド)。すでに実行している Scan が
問題なら、Scan が遅く高価な理由 が
トリアージの手順を案内します。
DynamoDB に対する反射的な SELECT * FROM table は、PartiQL の衣をまとった同じ
アンチパターンです — Scan にコンパイルされます。本当にアイテム横断の分析
(GROUP BY、JOIN、集計)が必要なときは、DynoTable の SQL Workbench が、テーブルを
叩く代わりに、限定された結果セットに対してクライアント側でそれらを実行します。
DynoTable を試して、これらのクエリを自分のテーブルに対して実行・ 検査してみてください — 実行するすべての操作の消費キャパシティが表示されます。
Scan 反射からモデル化された Query へ
SQL から来たチームは、まず Scan に手を伸ばしがちです。直し方はほぼいつも構造的です。
- パーティションに名前を付ける — 何の id が読み取りをスコープするか(
USER#42、GAME#7)? - パーティションキーがベーステーブルと違うなら GSI を追加する — GSI vs LSI を参照。
- Query を吐き出す — クエリビルダー がインデックス、キー条件、射影、フィルタ、Limit、ページネーションを1本の実行可能なプログラムにまとめます。
複数のエンティティ型が1テーブルを共有するなら、シングルテーブル設計ツール でオーバーロードされたキーをスケッチしてください。プランナーは、どのパターンがパーティションクエリになり、どれがなおスキャンを要するかを示します。
ベーステーブル上の強い整合性 Query
ベーステーブルまたは LSI への Query は、書き込み後の鮮度が要るとき ConsistentRead=true を受け付けます。GSI は結果整合性のままです — フィルタ付き GSI で Scan を置き換えるパターンでも、インデックスを強い整合性で読むことはできません。整合性モード で 2× RCU のトレードオフを確認してください。
キーなしの PartiQL SELECT はなお Scan
パーティションキー述語のない SELECT * FROM "Orders" はテーブル Scan にコンパイルされます — API レベルの Scan と同じコストです。pk がテーブルのパーティションキーなら、WHERE pk = 'U#1' を足すと Query になります。FAQ DynamoDB は SQL をサポートしますか? は、DynamoDB が実装しない SQL 機能を求めたときのパーサエラーを引用しています。
リファクタ中の可観測性
Scan 呼び出しを置き換えるあいだ、ReturnConsumedCapacity: TOTAL をオンにします。インデックス追加の前後でエンドポイントごとの ConsumedCapacity.CapacityUnits をログしてください — テーブル規模の読み取りからパーティション規模への落ち込みが、モデリング変更の証拠です。
DynoTable は UI で各操作の消費キャパシティを出すので、リファクタ中にステージングテーブルとの並べ比較が速くなります。
Scan がなお正当なとき
| シナリオ | Scan でよい理由 | 緩和策 |
|---|---|---|
| テーブル全体の夜間エクスポート | フル読み取りが目的 | 並列セグメント + オフピーク帯 |
| 1 MB 未満の設定テーブル | コストが無視できる | 可能なら既知キーの GetItem を優先 |
| 1回限りの移行監査 | まれで有界 | LastEvaluatedKey でページ送り、WCU を絞る |
| キーにモデル化されていない分析 | 正直なフル読み取り | 有界エクスポート上の DynoTable SQL Workbench |
繰り返しの分析なら、スケジュールでテーブル規模の読み取りを払う代わりに、サマリアイテムやストリーム由来の集計をモデル化してください。