DynamoDB の Query と Scan:どちらをいつ使うか
Query はパーティションキーで単一のアイテムコレクションを読み取り、オプションでソートキーで絞り込みます。Scan は テーブル全体 を読んでから後でフィルタします。API では似て見えますが、課金とスケールはまったく異なります。
DynamoDB で Query と Scan をいつ使い分けるべきか?
必要なパーティションキーの値を名指しできるときは、いつでも Query を使いましょう。1 つのアイテムコレクションを読み取り、キー条件が選んだアイテム分だけ課金されます。Scan はすべてのアイテムに触れる必要があるジョブか、ごく小さなテーブルにだけ使ってください。FilterExpression が実行される前に、すべてのアイテムを読み取って課金するからです。
- フィルタで請求額が減ることはありません。
FilterExpressionは、どちらの操作でも読み取りが計測された後に動きます。 - 全件読み取りなら、まず S3 へのエクスポートを検討しましょう。 読み取りキャパシティを一切消費せずにテーブル全体をコピーします。
並べて比較
| Query | Scan | |
|---|---|---|
| 読み取り | 1 つのアイテムコレクション(1 つのパーティションキー値) | テーブル内の すべての アイテム |
| 課金キャパシティ | キー条件が選んだアイテム | フィルタ前の テーブル全体 |
FilterExpression | 読み取り後に適用され、読み取り分は課金される | 同様: フィルタはコストを削らない |
| レイテンシ | テーブルが成長しても一定 | テーブルサイズとともに増加 |
| ページネーション | 1 MB/ページ、続きは LastEvaluatedKey | 1 MB/ページ。セグメントで並列化可能 |
| 用途 | 既知のアクセスパターン | バックフィル、小さな設定テーブル |
「10 行を返す」Scan が、100 万行の読み取り分を課金することがあります。フィルタは便宜であって、決してコスト管理ではありません。
フル Scan の実際のコスト
DynamoDB は読み取りを 4 KB 単位で計測します。強い整合性のある読み取りは 4 KB ごとに 1 読み取りユニットを消費し、結果整合性のある読み取りはその半分です。
Query と Scan は、返したアイテムではなく 読み取った すべてのアイテムのサイズを合計し、ページの合計を次の 4 KB に切り上げます(AWS: 読み取りおよび書き込みオペレーション)。
1 アイテム平均 2 KB の 100 万アイテムのテーブル(データ約 2 GB)と、そのうち 10 アイテムを必要とするアクセスパターンを考えてみましょう。
| 読み取りアイテム数 | 計測データ量 | 読み取りユニット(結果整合性) | |
|---|---|---|---|
Scan + FilterExpression | 1,000,000 | 約 2 GB | 250,000 |
マッチするキーでの Query | 10 | 20 KB | 2.5 |
同じ 10 アイテムで、5 桁もの差です。
Scan は、フィルタが 10 アイテムにマッチしても 1 件もマッチしなくても、実行のたびにその 250,000 読み取りユニットを課金します。目安として、Scan が読み取る 1 GiB ごとに、結果整合性で 131,072 読み取りユニット(強い整合性で 262,144)かかります。
オンデマンドテーブルでは、それらがそのまま読み取りリクエストユニットとして請求に乗ります。このテーブルの Scan 1 回は us-east-1 の定価(100 万あたり $0.125)で約 $0.03 ですが、ジョブが 5 分ごとに実行すれば月に約 $270 になります。
プロビジョンドテーブルでは、Scan は本番トラフィックと同じ読み取りキャパシティを消費し、本番トラフィックをスロットリングして ProvisionedThroughputExceededException を引き起こすことがあります。
ジョブがすべてのアイテムを必要とするなら、エクスポートとして実行してください(後述)。テーブルのキャパシティにはまったく触れません。
ジョブがライブテーブルを読まなければならないなら、ペースを落としましょう。ページごとに小さな Limit、ページ間の待機、少数のワーカーです。行き過ぎたときのリトライはスロットリングで解説しています。
さらに、コストの事実を 3 つ:
Select: COUNTはタダではありません。 件数を数える Query や Scan は、アイテムを読み取るのと同じ読み取りユニットを消費し、結果全体ではなくページごとに数えます。Limitが制限するのは 評価された アイテム数で、返されるアイテム数ではありません。 フィルタと組み合わせると、ページが空で返ってきてもLastEvaluatedKeyが付いていることがあります。キーがなくなるまでページ送りを続けてください。ループの書き方はページネーションで解説しています。- 推測する必要はありません。
ReturnConsumedCapacity: TOTALを渡せば、すべてのレスポンスが消費したキャパシティを報告します。
自分のアイテムの重さはアイテムサイズ計算ツールで確認し、読み取りユニットを月額の料金に換算するには料金計算ツールを使ってください。
Query を使う
Query PK = "TEAM#billing" AND SK begins_with "TICKET#"
選択は 1 つの問いに帰着します。必要なパーティションキーの値を名指しできますか?
SQL から来ると、まず Scan に手を伸ばしがちです。直し方はほぼいつも構造的です。
- パーティションに名前を付ける。 何の id が読み取りをスコープするか(
TEAM#billing、GAME#7)? - その id がテーブルのパーティションキーでないなら GSI を追加する。 GSI vs LSI を参照してください。GSI は結果整合性の読み取りしか提供しない(GSI に
ConsistentRead=trueを指定するとValidationException)ので、パターンに書き込み直後の鮮度が必要なら整合性モードを確認してください。 - Query を出力する。 クエリビルダーがインデックス、キー条件、射影、フィルタ、Limit、ページネーションを 1 本の実行可能なプログラムにまとめます。
複数のエンティティ型が 1 テーブルを共有するなら、シングルテーブル設計ツールでオーバーロードされたキーをスケッチしてください。プランナーは、どのパターンをベーステーブルが提供でき、どれに GSI が必要で、どれがきれいに提供できないかを示します。
フィルタがキー以外の属性に対するものなら、フィルタリング戦略で読み取りを絞る 4 つの方法を比較しています。
PartiQL でもこれは何も変わりません。WHERE にパーティションキーの = も IN もない SELECT はフル Scan です。Scan が遅く高価な理由で、キーを固定する方法と、IAM で意図しないフルスキャンをブロックする方法を示しています。
Scan で問題ないとき
ジョブがすべてのアイテムに関わるものなら、全件読み取りは正当です。ジョブがそれらのアイテムで何をするかでツールを選びましょう。
| ジョブ | 使うもの | 理由 |
|---|---|---|
| テーブル全体を S3 やデータウェアハウスにコピーする | S3 へのエクスポート(フル、または増分: 15 分から 24 時間のウィンドウ) | PITR バックアップを読むので、読み取りキャパシティを消費しない |
キー化されていないアドホック分析(GROUP BY、結合) | S3 へエクスポートしてから Athena(次のセクション) | エクスポートしたファイルに SQL を実行し、テーブルは読まない |
| 毎日必要な件数や合計 | Streams + Lambda で最新に保つサマリアイテム(リプレイ対策付き) | レポートごとに GetItem 1 回 |
| ライブアイテムを書き換えるバックフィルや移行 | 並列 Scan、LastEvaluatedKey でページ送り、ペース調整、オフピーク | エクスポートはテーブルに書き戻せない |
| 1 MB 未満の設定テーブルやルックアップテーブル | Scan 1 ページ、またはキーがわかっているなら GetItem | コストが無視できる |
ライブテーブルのジョブでは、Segment/TotalSegments を使って Scan をワーカー間で分割し(並列スキャン、DynamoDB の並列スキャンを参照)、LastEvaluatedKey でページ送りしてください(ページネーションガイド)。
すでに実行している Scan が問題なら、Scan が遅く高価な理由がトリアージの手順を案内します。
毎日の件数には DynamoDB Streams を有効にし、各変更をサマリアイテムに適用する Lambda を付けます。リトライされたバッチが二重に数えられないよう、レコードごとのマーカーでガードします。ハンドラーは Streams + Lambda のロールアップで示しています。
キーのない分析: エクスポートしてから Athena
S3 へのエクスポートは、テーブルをポイントインタイムリカバリ(PITR)バックアップからコピーします。PITR を有効にする必要があり、読み取りキャパシティは消費せず、エクスポートしたデータ量で課金されます(AWS: S3 へのエクスポート)。
# Once per table: turn on point-in-time recovery
aws dynamodb update-continuous-backups --table-name AuditLog \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
# Export the current state as gzipped DynamoDB JSON
aws dynamodb export-table-to-point-in-time \
--table-arn arn:aws:dynamodb:us-east-1:123456789012:table/AuditLog \
--s3-bucket my-export-bucket --s3-prefix auditlog \
--export-format DYNAMODB_JSONエクスポートされた各行は、1 つのアイテムを DynamoDB JSON の型付きで Item キーに包んでいます。Athena のテーブルをエクスポートの data/ フォルダーに向け、SQL でクエリします(AWS: エクスポートの出力形式)。
CREATE EXTERNAL TABLE audit_export (
Item struct<PK:struct<S:string>, action:struct<S:string>>
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://my-export-bucket/auditlog/AWSDynamoDB/<export-id>/data/';
SELECT Item.action.S AS action, count(*) AS events
FROM audit_export
GROUP BY Item.action.S;Athena は S3 でスキャンしたデータ量で課金し、テーブルはクエリをまったく受けません。
より小さなサブセットなら、フィルタ付きエクスポートでキー条件とフィルタを指定できます。出力形式の詳細はエクスポートガイドで解説しています。
キー化できるスライスについての 1 回限りの質問なら、DynoTable の SQL Workbench がライブテーブルに対して、手元のマシン上で GROUP BY、JOIN、集計を実行します。
ベーステーブルにパーティションキーの条件を与えれば Query で読み取り、与えなければそのテーブルを Scan し、その読み取り分を支払います。どちらになるかは、実行前にステータスバーが教えてくれます。
次のステップ
Scan 呼び出しを置き換えるあいだは ReturnConsumedCapacity: TOTAL をオンにし、インデックス追加の前後でエンドポイントごとの ConsumedCapacity.CapacityUnits をログしてください。
テーブル規模の読み取りからアイテムコレクション規模の読み取りへの落ち込みが、モデリング変更がうまくいった証拠です。
DynoTable を試して、これらの読み取りを自分のテーブルに対して実行してみてください。グリッドは各実行でスキャンしたアイテム数を返したアイテム数の隣に表示し、Workbench と PartiQL エディターはプランがわかっていれば実行前にクエリの読み取りユニットを見積もります。