DynamoDB の Scan が遅くて高コストな理由
Scan はテーブル内のすべてのアイテムを読み取り、その後でしかフィルタリングしません。
それは SQL の筋肉記憶で手を伸ばしてしまう操作であり、そして、あなたが後にしてきた RDS ボックスよりも
レイテンシーを悪化させながら、静かに請求額をつり上げる操作です。
私の DynamoDB の Scan はなぜ遅くて高コストなのか?
Scan は FilterExpression が動く前にテーブル内のすべてのアイテムを読み取るため、返ってくる行が
どれほど少なくてもテーブル全体を読み取る分を支払うことになり、テーブルが大きくなるほど遅くなります。
修正策はほぼ常にキー付きの Query です — アクセスパターンをキーを中心にモデリングして、DynamoDB が
すべてではなく1つのパーティションに触れるようにします。
Scanは毎回テーブル全体を読み取る。 結果件数ではなくサイズが、支払う額と所要時間を決めます。FilterExpressionはコストについての嘘。 それは読み取りが計量された 後 に動くので、12件のアイテムを 返すのに1200万件の読み取り分を請求されることがあります。Scanは成長するほど遅くなる。 キー付きのQueryは横ばいのままです — テーブルがどれだけ大きくなっても 1つのパーティションに触れるだけです。- 修正策はほぼ常にモデリングであって、チューニングではない。 日常的な質問に答えるために
Scanするなら、 あなたはキーを見落としています。
Scan が実際にすること
SQL から来ると、SELECT * FROM events WHERE type = 'checkout' はタダに感じられます —
エンジンにはインデックスがあるか、ないかのいずれかですが、どちらにしても行が返ってきます。
DynamoDB にはそれを決めてくれるクエリプランナーはありません。
Scan はテーブル全体を 1 MB ずつ順に歩き、各ページをあなたの FilterExpression に渡します。
フィルターが拒否したものも読み取られ、計量され、なおあなたの請求に乗ります。
(AWS: テーブルのスキャン)
それが罠です。フィルターは WHERE 句のように見えますが、変えるのは結果セットであって、コストではありません。
Scan はフィルターの有無に関わらず同じ読み取りキャパシティを消費します。(AWS: テーブルのスキャン)
読み取りユニットを数える
DynamoDB は読み取りを (RCU) で計量します。1 RCU は最大 4 KB の アイテムの 読み取り1回分に相当します。 読み取りは その半分のコストです。より大きなアイテムは次の 4 KB に切り上げられます。(AWS: 読み取り/書き込み キャパシティモード)
分析用テーブル ProductEvents を取り上げます。各行は追跡された1つのイベントです。
PK = "TENANT#acme"
SK = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytesこれが1つの多忙なテナントの下に、それぞれ約 1 KB の 2,000,000 件のイベントを保持しているとしましょう。 あなたは今日のチェックアウトを求めます。反射的な手は次のとおりです。
Scan ProductEvents
FilterExpression: eventType = "checkout"
そのフィルターは40行を返すかもしれません。しかし Scan はまず 2,000,000 件のアイテムをすべて読み取りました。
それぞれ約 1 KB(4 KB あたり 1 RCU、結果整合性で 4 KB あたり約 0.5 RCU)で、40件のアイテムを返すために
およそ 250,000 RCU を計量し — 約 2 GB のデータをページングしました。
今度はアクセスパターンをキーとしてモデリングし、代わりに Query します。
Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"
これは1つのパーティションの、一致したスライスだけを読み取ります。それら40件のチェックアウト行に、その日の 他のイベントを加えて約 2 MB になるなら、支払うのは 2 GB ではなく約 2 MB の読み取り分です。同じ答え、コストは ごくわずか — そしてテーブルが成長してもレイテンシーは横ばいのままです。
Scan 対 Query、計量した比較
| Scan + フィルター | キー付き Query | |
|---|---|---|
| 読み取り | テーブル内のすべてのアイテム | 1つのパーティション、SK で絞り込み |
| 課金対象キャパシティ | フィルター前のテーブル全体 | スライス内のアイテムのみ |
| この例 | 約 250,000 RCU(約 2 GB) | 数百 RCU(約 2 MB) |
| レイテンシー | テーブルサイズとともに増大 | テーブルが成長しても横ばい |
| 結果件数 | コストについて何も決めない | 支払う額と一致する |
この表が刻む教訓はこうです。Scan では結果件数と請求額は無関係です。Query では、両者は互いに追随します。
Scan する前に決める
偶発的な Scan のほとんどは1つの問いから生まれます。必要なパーティションを名指しできるか?
できるなら Query です。できないなら、修正策はより大きなフィルターではなくキーです。
以下がその判断をフロー図にしたものです。
その道はほぼ常に Query で終わります。キー — 既存のものでも追加できるものでも — がアクセスパターンに
合わないときにだけ、Scan へと落ちていきます。
パターンが現実的で反復的なのにベーステーブルではキー付けできないなら、それは
グローバルセカンダリインデックス を追加して、その質問を Query にするシグナルです。
アクセスパターンを中心にキーを前もってモデリングすることがすべてです — シングルテーブル設計
を参照してください。
フィルターではなく、キー付きのクエリを書く
キーを超える条件が本当に必要なときは、すべてを FilterExpression に投げ込むのではなく、意図的に組み立てましょう。
DynamoDB 式ビルダー が KeyConditionExpression と属性の
プレースホルダーを生成してくれるので、DynamoDB が読み取りを計量する 前 に、パーティションキーとソートキーが
絞り込みを行います — 後ではなく。
KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)
Scan が実際に妥当なとき
Scan は禁じられているわけではありません — ただ、デフォルトとして間違っているだけです。本当に
「すべてを読む」つもりなら、それが正しいツールです。
- 1回限りのエクスポートや手作業のバックフィル。
- テーブル全体が数 KB しかない、小さな設定/ルックアップテーブル。
- 意図的にテーブル全体をページングするバックグラウンドジョブ。それらは1つの長い逐次的なクロールではなく、
Segment/TotalSegmentsでワーカー間に分割しましょう — です。(AWS: テーブルのスキャン)
そして PartiQL も救ってはくれないことに注意してください。キー述語のない SELECT * FROM ProductEvents WHERE eventType = 'checkout' はまっすぐ Scan にコンパイルされます。それは SQL の衣をまとった同じ落とし穴です。
(完全な内訳は Query 対 Scan を参照してください。)
DynamoDB が表現できない GROUP BY、JOIN、集計といったアイテム横断の分析が本当に必要なときは、
DynoTable の SQL Workbench がそれらを、テーブルをフル Scan で叩くのではなく、境界のある結果セットに対して
クライアント側で実行します。
次のステップ
料金計算ツール でどちらのパターンにいくらかかるかを見積もり、 API レベルの対比については Query 対 Scan を読み、そして DynoTable をダウンロード して、これらを自分自身のテーブルに対して実行し、各アプローチが 実際に何件のアイテムを読み取るかを確かめましょう。