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 を試して、これらのクエリを自分のテーブルに対して実行・ 検査してみてください — 実行するすべての操作の消費キャパシティが表示されます。