なぜ 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: Scanning tables)
それが罠です。フィルタは WHERE 句に見えますが、結果セットを変えるだけで、コストは決して
変えません。Scan は、フィルタがあろうとなかろうと、同じ読み取りキャパシティを消費します。
(AWS: Scanning tables)
読み取りユニットを数える
DynamoDB は読み取りを 読み取りキャパシティユニット(RCU) で計測します。1 RCU は、最大 4 KB のアイテムの強い整合性のある読み取り1回を買います。結果整合性の読み取りはその半分です。大きな アイテムは次の 4 KB に切り上がります。(AWS: Read/write capacity mode)
分析テーブル ProductEvents を取り上げます。各行は1つの追跡イベントです。
PK = "TENANT#acme"
SK = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytesそれが 2,000,000 件のイベントを保持し、各々約 1 KB で、すべて1つの忙しいテナントの 下にあるとします。今日のチェックアウトが欲しいとします。反射的な一手:
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 を計測し — 約 500 MB のデータをページ送りしました。
今度はアクセスパターンをキーとしてモデリングし、代わりに Query します。
Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"
これは1つのパーティションのマッチしたスライスだけを読みます。それら 40 件のチェックアウト 行とその日の他のイベントが約 2 MB になるなら、500 MB ではなく約 2 MB の読み取りを支払い ます。同じ答え、ごくわずかなコスト — そしてテーブルが成長してもレイテンシは平坦なまま です。
Scan と Query、計測して
| Scan + フィルタ | キーイングされた Query | |
|---|---|---|
| 読み取り | テーブル内の すべての アイテム | 1つのパーティション、SK で絞り込み |
| 課金キャパシティ | フィルタ前のテーブル全体 | スライス内のアイテムだけ |
| 私たちの例 | 約 250,000 RCU(約 500 MB) | 数百 RCU(約 2 MB) |
| レイテンシ | テーブルサイズとともに増加 | テーブルが成長しても平坦 |
| 結果件数 | コストについて何も決めない | 支払うものと一致する |
この表がエンコードする教訓:Scan では、結果件数と請求は無関係です。Query では、それらは
互いを追跡します。
Scan する前に決める
たいていの偶発的な Scan は1つの問いから来ます。必要なパーティションを名指しできるか?
できるなら、それは Query です。できないなら、修正はより大きなフィルタではなくキーです。
フロー図にした決定がこちらです。
その道はほぼ常に Query で終わります。キー — 既存のものでも追加可能なものでも — が
アクセスパターンに合わないときだけ、Scan に落ちます。
パターンが実在し繰り返されるのにベーステーブルがキーイングできないなら、それは
グローバルセカンダリインデックス を追加して問いを Query にする
サインです。アクセスパターンを中心に前もってキーをモデリングすることがゲームのすべてです
— シングルテーブル設計 を参照してください。
フィルタではなく、キーイングされたクエリを書く
キーを超えた条件が本当に必要なときは、すべてを FilterExpression に詰め込むのではなく、
意図的に構築しましょう。DynamoDB Expression Builder
が KeyConditionExpression と属性プレースホルダーを生成するので、パーティションキーと
ソートキーが絞り込みをします — DynamoDB が読み取りを計測する後ではなく、前に。
KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)
Scan が実際に問題ないとき
Scan は禁止ではありません — ただ間違ったデフォルトなだけです。本当に「すべてを読む」を
意味するときには、正しい道具です。
- 手で実行する1回限りのエクスポート やバックフィル。
- 小さな設定/参照テーブル — テーブル全体が数 KB のところ。
- バックグラウンドジョブ — 意図的にテーブル全体をページ送りするもの。1本の長い順次
クロールではなく、
Segment/TotalSegmentsでワーカー間に分割します — 並列スキャン です。(AWS: Scanning tables)
そして PartiQL は救ってくれないことに注意してください。キー述語のない
SELECT * FROM ProductEvents WHERE eventType = 'checkout' は、まっすぐ Scan に
コンパイルされます。SQL の衣をまとった同じ落とし穴です。(完全な分解は
Query と Scan を参照。)
DynamoDB が表現できないアイテム横断の分析 — GROUP BY、JOIN、集計 — が本当に必要な
ときは、DynoTable の SQL Workbench が、テーブルを全 Scan で叩く代わりに、限定された
結果セットに対してクライアント側でそれらを実行します。
次のステップ
キャパシティ計算機 でどちらのパターンがどれだけ かかるか見積もり、API レベルの対比は Query と Scan を読み、 DynoTable をダウンロード して自分のテーブルに対してこれらを実行し、消費した キャパシティを自分の目で見てください。