# DynamoDB キャパシティ・コスト・性能設計 完全ガイド（2026年版）：オンデマンド vs プロビジョンド、Auto Scaling、ホットパーティション回避、コスト最適化

> DynamoDBの料金と性能を決めるキャパシティ設計を、AWS公式仕様に忠実に解説。オンデマンドvsプロビジョンドの損益分岐、RCU/WCUの正しい数え方、3000/1000のパーティション上限とホットキー回避、ウォームスループット、Auto Scaling、TTL・テーブルクラスによるコスト最適化までを、Terraform/AWS SDK v3の実コードで本番目線にまとめます。

- 公開日: 2026-06-25
- 著者: 友田 陽大
- タグ: AWS, DynamoDB, アーキテクチャ設計, サーバーレス, Terraform, TypeScript, コスト最適化
- URL: https://tomodahinata.com/blog/dynamodb-capacity-cost-performance-on-demand-vs-provisioned-guide

## 要点

- 課金モードは2つだけ。スパイク/予測不能ならオンデマンド、定常/予測可能ならプロビジョンド＋リザーブド。後から相互に変更可（プロビジョンド→オンデマンドは24時間で最大4回、逆はいつでも）
- オンデマンドの定価はプロビジョンド(100%稼働換算)の約3.46倍。損益分岐は持続稼働率およそ30%で、リザーブドキャパシティ（1年最大54%/3年最大77%割引）で分岐点はさらに下がる
- 1パーティションの上限は毎秒3,000読み取り/1,000書き込みユニット。これを超える偏りがホットパーティション。高カーディナリティなキー設計とライトシャーディングで分散する。Adaptive Capacityは全テーブルで自動・無料だが上限は超えられない
- コストは『消費の数え方』で決まる。UpdateItemは前後の大きい方、条件失敗でも書き込み課金、FilterExpression/Scanは読んだ分だけ課金。ReturnConsumedCapacityで実測し、ProjectionExpressionとQueryで削る
- TTL削除はWCUを消費せず無料。打ち上げ前はウォームスループットで事前ウォーミング。Auto Scalingは目標使用率70%が定石だがスパイクには数分遅れる——バーストとオンデマンドで吸収する

---

DynamoDBで最初につまずくのは「正しさ」ではありません。**「料金」と「詰まり（スロットリング）」**です。

データモデルは綺麗に設計できた。冪等性も条件付き書き込みも入れた。なのに本番で——請求が予想の3倍に膨らんだり、セール開始の瞬間に `ProvisionedThroughputExceededException` が噴き出したりする。これはコードのバグではなく、**キャパシティ設計の問題**です。DynamoDBの料金と性能は、テーブルの「課金モード・キャパシティ・キー設計」という3つの選択でほぼ決まります。

本記事は、私が**AWSサーバーレス（Lambda + DynamoDB）のマルチテナント決済プラットフォーム**を本番運用してきた経験をベースに、DynamoDBの**キャパシティ・コスト・性能設計**だけを体系化したものです。データモデリングや冪等性・トランザクションといった「正しさ」の設計は、姉妹記事の[DynamoDB シングルテーブル設計＆本番信頼性パターン完全ガイド](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)に譲ります。本記事はそれと相補的に、**「いくらかかるか」「どこで詰まるか」「どう速く・安くするか」**に絞ります。

数値・上限はすべて **AWS公式ドキュメント（2026年6月時点）** に照合しています。料金はリージョン・時期で変わるため、最終的な金額は必ず[公式料金ページ](https://aws.amazon.com/dynamodb/pricing/)で確認してください。本文の価格はすべて **US East (N. Virginia) / 標準テーブルクラス / 2026年6月時点**です。

---

## 1. 課金モードは2つしかない：オンデマンド vs プロビジョンド

DynamoDBの料金体系の出発点は、テーブルごとに選ぶ**スループットモード**です。これが「どう課金されるか」と「どう自動スケールするか」を同時に決めます。

| 観点 | オンデマンド（On-Demand） | プロビジョンド（Provisioned） |
| --- | --- | --- |
| 課金単位 | 実際のリクエスト（RRU / WRU） | 確保した容量（RCU / WCU・時間課金） |
| 課金の考え方 | 使った分だけ（ゼロトラフィックなら0円） | 使わなくても確保分は課金 |
| スケール | 完全自動。前ピークの2倍まで即時 | 手動 or Auto Scaling |
| キャパシティ計画 | 不要 | 必要（予測が前提） |
| 向いている負荷 | スパイク・予測不能・新規・開発環境 | 定常・予測可能・高稼働率 |
| 単価（同一消費量あたり） | 高い（プロビジョンド100%換算の約3.46倍） | 安い（ただし高稼働率を維持できれば） |

公式は明言しています。**オンデマンドが「デフォルトかつ推奨」**です。

> On-demand mode is the default and recommended throughput option for most DynamoDB workloads.（オンデマンドは、ほとんどのDynamoDBワークロードにとってデフォルトかつ推奨のスループットオプションである）

「とりあえず動かす」「トラフィックが読めない」段階では迷わずオンデマンド。最適化は実測値が貯まってから——というのが正しい順序です。早すぎる最適化（プロビジョンドの当てずっぽうな容量設定）は、コスト増かスロットリングのどちらかを招きます。

### オンデマンドの本質：「前ピークの2倍まで即時」

オンデマンドは魔法ではありません。スケールには明確なルールがあります。

- **新規テーブルの初期スループット**：作った直後から **毎秒4,000書き込み・12,000読み取り** を即座にさばける。
- **前ピークの2倍まで即時**：過去に到達したトラフィックのピークの2倍までは、いつでも瞬時に出せる。例えばピークが毎秒5万読み取りなら、毎秒10万まで即時。10万を出せばそれが新しいピークになり、次は20万まで伸ばせる。
- **2倍を超える急増には30分ルール**：前ピークの2倍を**30分以内**に超えようとするとスロットリングし得る。公式は「トラフィック増は30分かけて広げるか、事前ウォーミングせよ」と明記。

つまりオンデマンドでも、**セールや打ち上げで一気に10倍・100倍**になるイベントは要注意です。対策は後述の[ウォームスループット](#5-ウォームスループットで打ち上げを乗り切る)。

なお、オンデマンドにも**デフォルトのテーブル単位クォータ（毎秒40,000 RRU / 40,000 WRU）**があります。これは暴走防止のガードレールで、申請で引き上げられます（オンデマンドにアカウント単位のスループットクォータはありません）。

### プロビジョンドの本質：「確保した容量に時間課金」

プロビジョンドは、**毎秒の読み取り（RCU）・書き込み（WCU）容量を自分で確保**し、その確保量に時間課金されます。**使い切らなくても課金される**のがオンデマンドとの決定的な違いです。その代わり単価は安く、リクエストレートを上限で律速できるのでコストの予測可能性が高い。

- デフォルトクォータ：テーブル単位 40,000 RCU / 40,000 WCU、アカウント単位 80,000 RCU / 80,000 WCU（いずれも申請で増枠可）。最小は 1 RCU / 1 WCU。
- **容量の減少には回数制限**：1日は4回の「減少枠」で始まり、1時間ごとに1枠回復（最大4枠）。24時間で最大27回まで減らせる。**増加は無制限**。

この「減少は1日27回まで」という非対称性は、後述のAuto Scalingがスケールダウンに慎重な理由でもあります。

### モードは後から相互に変更できる（ただし回数制限あり）

決め打ちで失敗しても、やり直せます。

- **プロビジョンド → オンデマンド**：24時間のローリングウィンドウで**最大4回**まで。
- **オンデマンド → プロビジョンド**：**いつでも**。

切り替え時の挙動も理解しておくと安全です。プロビジョンド→オンデマンドに初めて切り替えると、テーブルは**少なくとも毎秒4,000書き込み・12,000読み取り**（過去にそれ以上を確保していたならその値）を即時に出せる状態にスケールされます。逆方向では、オンデマンド時の**前ピークに見合った**スループットで提供されるので、戻すときは初期プロビジョンド値を高めに設定して移行を吸収します。

> **実務指針**：新規・PoC・開発環境はオンデマンドで始める。本番で数週間〜1か月の `ConsumedReadCapacityUnits` / `ConsumedWriteCapacityUnits` をCloudWatchで観測し、**定常的に高稼働率**だと分かったテーブルだけプロビジョンド＋リザーブドへ寄せる。判断基準は次章の損益分岐です。

---

## 2. キャパシティの「数え方」を制す者がコストを制す

オンデマンドもプロビジョンドも、課金の土台は同じ**キャパシティユニットの消費量**です。ここを正確に数えられないと、料金もスロットリングも予測できません。公式の定義はシンプルです。

**読み取り（1ユニット = 最大4KBのアイテムに対し）**

| 読み取りの種類 | 消費ユニット（4KBまで） |
| --- | --- |
| 結果整合性（デフォルト） | 0.5 |
| 強整合性 | 1 |
| トランザクション（TransactGetItems） | 2 |

**書き込み（1ユニット = 最大1KBのアイテムに対し）**

| 書き込みの種類 | 消費ユニット（1KBまで） |
| --- | --- |
| 通常（Put/Update/Delete） | 1 |
| トランザクション（TransactWriteItems） | 2 |

サイズは**読み取りは4KB単位・書き込みは1KB単位で切り上げ**ます。3.5KBのアイテムを読めば4KB扱い、10KBなら12KB扱い。500バイトを書いても1KB分を消費します。

### 請求を膨らませる4つの「数え方の罠」

公式仕様を読み込むと、初見では見落としがちな課金ポイントが見えてきます。**これらはバグではなく仕様**で、知らないと静かにコストを蝕みます。

1. **`UpdateItem` は「更新前と更新後の大きい方」で課金される。** 1属性だけ書き換えても、アイテム全体のサイズで課金。巨大アイテムの頻繁な部分更新は高コスト。
2. **条件付き書き込みは、失敗しても書き込みキャパシティを消費する。** `ConditionExpression` が `false` でも、対象アイテムのサイズ分のWCUが課金される。冪等性チェックの空振りも有料という前提でリトライ設計を。
3. **`FilterExpression` は「読んだ後」に絞るだけ。課金は読んだ分。** フィルタで結果が0件でも、スキャン/クエリした全アイテム分の読み取りユニットを消費する。フィルタは節約手段ではない。
4. **`Scan` は「返したサイズ」ではなく「評価したサイズ」で課金される。** テーブル全走査は、返り値が1件でもテーブル全体を読んだ分だけ課金。**本番のホットパスでScanは原則禁止**。

> 補足：`Query` は同一パーティションキーの複数アイテムを1回の読み取りとして合算し、4KB単位で切り上げます。例えば64バイト×1,500件のクエリは合計96KB＝24読み取りユニット（結果整合なら12）。**「少しずつ大量に」は意外と高い**ことが分かります。

### 数えるな、測れ：`ReturnConsumedCapacity`

理論で数えるより、**実測**が正確です。AWS SDK for JavaScript v3 では、リクエストに `ReturnConsumedCapacity` を付けるだけで、その操作が消費したユニットを返してくれます。コスト最適化は必ずここから始めます。

```ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

/** あるアクセスパターンが実際に何ユニット消費するかを本番相当データで実測する。 */
export async function measureQueryCost(pk: string): Promise<number> {
  const res = await ddb.send(
    new QueryCommand({
      TableName: "AppTable",
      KeyConditionExpression: "PK = :pk",
      ExpressionAttributeValues: { ":pk": pk },
      // ← これを付けるだけで消費キャパシティが返る（"TOTAL" | "INDEXES" | "NONE"）
      ReturnConsumedCapacity: "TOTAL",
      // 取得属性を絞ると、転送量は減るが「読み取りユニットは変わらない」点に注意。
      // 読み取りコストを下げる本丸は、アイテムを小さく保つこと。
      ProjectionExpression: "PK, SK, amount, #s",
      ExpressionAttributeNames: { "#s": "status" },
    }),
  );
  return res.ConsumedCapacity?.CapacityUnits ?? 0;
}
```

ここで重要な落とし穴：**`ProjectionExpression` で属性を絞っても読み取りユニットは減りません**（公式：取得属性のサブセット指定はアイテムサイズ計算に影響しない）。ネットワーク転送と帯域は減りますが、課金の本丸である**読み取りユニットを下げる手段は「アイテム自体を小さく保つ」こと**です。大きなBLOBはS3に逃がし、DynamoDBには参照（S3キー）だけ置く——が定石になります。

---

## 3. コスト設計：オンデマンド vs プロビジョンドの損益分岐

ここが本記事の核心です。「結局どっちが安いのか？」に、定価から定量的に答えます。

**料金（US East / 標準テーブルクラス / 2026年6月）**

| 項目 | オンデマンド | プロビジョンド |
| --- | --- | --- |
| 書き込み | $0.625 / 100万 WRU | $0.00065 / WCU・時 |
| 読み取り | $0.125 / 100万 RRU | $0.00013 / RCU・時 |
| データストレージ（Standard） | $0.25 / GB・月（最初の25GBは無料利用枠） | 同左 |
| データストレージ（Standard-IA） | $0.10 / GB・月 | 同左 |
| 無料利用枠 | — | 25 RCU + 25 WCU + 25 GB |
| リザーブドキャパシティ | 非対応 | 1年で最大54%・3年で最大77%の割引 |

### 単価は「約3.46倍」、損益分岐は「稼働率およそ30%」

プロビジョンドの単価をオンデマンドと同じ「100万消費ユニットあたり」に換算してみます。1WCUを100%使い切れば、`3,600 × 730時間 = 2,628,000` 書き込み/月をさばけます。

- プロビジョンド（100%稼働）：`$0.00065 × 730 ÷ 2.628 ≈ $0.18 / 100万書き込み`
- オンデマンド：`$0.625 / 100万書き込み`

**つまりオンデマンドの定価は、プロビジョンドを100%使い切った場合の約3.46倍**。読み取りも同じ比率です。逆に言えば、**プロビジョンドが得をするのは「確保した容量を高稼働率で使い切れるとき」だけ**。損益分岐の稼働率は `1 ÷ 3.46 ≈ 29%` です。

この計算をコードで確かめられるようにしておくと、テーブルごとの判断が一瞬で済みます。

```ts
// US East / 標準テーブルクラス / 2026-06 時点。必ず公式料金ページで再確認すること。
const ON_DEMAND_PER_MILLION = { read: 0.125, write: 0.625 } as const; // RRU / WRU
const PROVISIONED_PER_UNIT_HOUR = { read: 0.00013, write: 0.00065 } as const; // RCU / WCU
const HOURS_PER_MONTH = 730;
const SECONDS_PER_HOUR = 3600;

type Kind = "read" | "write";

/**
 * 持続稼働率 utilization（0–1）における「100万消費ユニットあたり」コストを比較する。
 * 核心：オンデマンドは消費した分だけ、プロビジョンドは「確保した容量」に課金される。
 * よって稼働率が低いほどプロビジョンドは割高になる。
 */
export function comparePerMillion(kind: Kind, utilization: number) {
  if (utilization <= 0 || utilization > 1) {
    throw new RangeError("utilization は 0 より大きく 1 以下で指定する");
  }
  const onDemand = ON_DEMAND_PER_MILLION[kind];
  const unitsPerCapacityPerMonth = SECONDS_PER_HOUR * HOURS_PER_MONTH; // 100%稼働時 = 2,628,000
  const provisionedMonthlyPerUnit = PROVISIONED_PER_UNIT_HOUR[kind] * HOURS_PER_MONTH;
  const provisioned =
    (provisionedMonthlyPerUnit / (unitsPerCapacityPerMonth * utilization)) * 1_000_000;
  return {
    onDemand: Number(onDemand.toFixed(4)),
    provisioned: Number(provisioned.toFixed(4)),
    cheaper: provisioned < onDemand ? ("provisioned" as const) : ("on-demand" as const),
  };
}

comparePerMillion("write", 0.29); // ≈ { onDemand: 0.625, provisioned: 0.6226, cheaper: "provisioned" }
comparePerMillion("write", 0.7); //  ≈ { onDemand: 0.625, provisioned: 0.2579, cheaper: "provisioned" }
comparePerMillion("write", 0.1); //  ≈ { onDemand: 0.625, provisioned: 1.8062, cheaper: "on-demand" }
```

### 「100%稼働」は現実には作れない——実務の分岐点はもっと高い

ここで多くの解説が止まりますが、**プロビジョンドを100%稼働させるのは現実には不可能**です。スロットリングを避け、バーストの余白を残すために、Auto Scalingの**目標使用率は70%**に置くのが定石。すると実効単価は `$0.18 ÷ 0.70 ≈ $0.26 / 100万`、オンデマンド比でおよそ**2.4倍安い**にとどまります。しかもこの2.4倍は、**トラフィックが滑らかでAuto Scalingが追従できる**ことが前提です。

整理すると、判断はこうなります。

- **スパイクが激しい / 予測不能 / 新規 / 低稼働率（〜30%）** → **オンデマンド**。単価は高いが、確保のムダ・スロットリング・運用負荷が消える。多くのケースで結局これが安い。
- **定常的・予測可能・高稼働率（70%前後を維持できる）** → **プロビジョンド + Auto Scaling**。滑らかな負荷で真価を発揮。
- **動かないベースライン負荷が常時ある** → そのベース分だけ**リザーブドキャパシティ**（1年最大54%/3年最大77%割引）で買い、変動分をオンデマンドのテーブルに切り出す**ハイブリッド**が最安になりやすい。

> **コスト暴走へのガードレール**：オンデマンドのまま安心したいが青天井は怖い——そんなときは**オンデマンドの最大スループット**（テーブル/GSIごとに最大RRU・WRUを設定）を使います。設定値を超えるリクエストはスロットリングされ、**事故やバグによる請求爆発を構造的に防ぎます**。後述のTerraformで設定します。

---

## 4. 性能の正体：パーティションとホットキー

スロットリングの多くは「テーブル全体の容量不足」ではなく、**1つのパーティションへの集中（ホットパーティション）**で起きます。ここがDynamoDB性能設計の心臓部です。

### 1パーティションの上限は「3,000読み取り / 1,000書き込み」

公式の鉄則です。

> Every partition in a DynamoDB table is designed to deliver a maximum capacity of 3,000 read units per second and 1,000 write units per second.（DynamoDBの全パーティションは、毎秒最大3,000読み取りユニット・1,000書き込みユニットを提供するよう設計されている）

テーブル全体で十分な容量があっても、**特定のパーティションキーにアクセスが偏れば、その1パーティションの上限でスロットリング**します。しかもアイテムサイズが効きます。20KBのアイテムなら1回の強整合読み取りで5ユニット消費するので、そのキーには**毎秒600回**でパーティション上限に達します。

### DynamoDBが自動で助けてくれる2層：バーストとAdaptive Capacity

設計を語る前に、AWSが自動で吸収してくれる仕組みを正しく理解します。

- **バーストキャパシティ**：使い切らなかった容量を**最大5分（300秒）分**まで貯め、突発スパイクで消費できる。短い山はこれが吸収する（だからAuto Scalingは短時間スパイクに即応しなくてよい）。
- **Adaptive Capacity**：**全テーブルで自動・無料・常時有効**。偏ったアクセスを検知し、ホットパーティションへ自動でスループットを寄せる。さらに**頻繁アクセスのアイテムを別パーティションへ隔離**し、極端な場合は単一の人気アイテムを1パーティションに独占させて**パーティション上限（3,000 RCU / 1,000 WCU）まで**供給する。

重要なのは、**Adaptive Capacityはオンデマンドにもプロビジョンドにも効く**が、**パーティション上限（3,000/1,000）もテーブル総容量も超えられない**という点。つまり「単一キーに毎秒1,000を超える書き込み」を続ければ、どんなモードでも詰まります。自動機構に頼り切らず、**設計で分散させる**必要があります。

### 設計の原則：高カーディナリティなキーと「ライトシャーディング」

公式の第一原則は「全パーティションキーで均一なアクセスになるよう設計せよ」。具体的には**カーディナリティ（値の種類）が高く、アクセスが満遍なく散る**キーを選びます。`user_id` や `order_id` は良い候補、`status`（数種類しかない）や「今日の日付」は悪い候補です。

避けがたいホットキー（例：時系列で「当日分」に書き込みが集中する集計テーブル）には、公式が推奨する**ライトシャーディング**——キーに計算済みのサフィックスを付けて複数パーティションに分散させる手法——を使います。

```ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

const SHARD_COUNT = 10; // 「当日」への書き込みを 1/10 に分散。読み取りは全シャードを束ねる。

/** 決定的ハッシュで seed を 0..SHARD_COUNT-1 に写像（= 同じ seed は常に同じシャード）。 */
function shardOf(seed: string): number {
  let h = 0;
  for (let i = 0; i < seed.length; i++) h = (Math.imul(h, 31) + seed.charCodeAt(i)) >>> 0;
  return h % SHARD_COUNT;
}

/** 書き込み：日付PKにシャードサフィックスを付け、1パーティション集中を避ける。 */
export async function recordEvent(day: string, eventId: string, payload: unknown): Promise<void> {
  const pk = `EVENTS#${day}#${shardOf(eventId)}`; // 例: "EVENTS#2026-06-25#7"
  await ddb.send(
    new PutCommand({
      TableName: "AppTable",
      Item: { PK: pk, SK: `EVENT#${eventId}`, payload },
    }),
  );
}

/** 読み取り：全シャードを並列クエリ（scatter-gather）して結合する。 */
export async function listEventsForDay(day: string): Promise<unknown[]> {
  const shards = await Promise.all(
    Array.from({ length: SHARD_COUNT }, (_, shard) =>
      ddb.send(
        new QueryCommand({
          TableName: "AppTable",
          KeyConditionExpression: "PK = :pk",
          ExpressionAttributeValues: { ":pk": `EVENTS#${day}#${shard}` },
        }),
      ),
    ),
  );
  return shards.flatMap((r) => r.Items ?? []);
}
```

トレードオフは明確です。**書き込みは分散して詰まらなくなる**が、**読み取りは全シャードを舐めるので少し高く・複雑になる**。だから**書き込みが集中し、かつ読み取りが許容できる**集計・イベント収集のようなパターンにだけ適用します。「とりあえず全テーブルをシャーディング」はYAGNI違反です。キー設計の詳細は[シングルテーブル設計ガイド](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)を参照してください。

---

## 5. ウォームスループットで「打ち上げ」を乗り切る

[1章](#1-課金モードは2つしかない-オンデマンド-vs-プロビジョンド)で触れた「前ピークの2倍まで即時、それ以上は30分かけて」という制約。セール・新製品ローンチ・TV露出のように**事前に山が分かっている**なら、**ウォームスループット**で先に温めておけます。

> **ウォームスループット**とは、テーブル/GSIが**今この瞬間に即座にさばける読み書きの量**。全テーブルにデフォルトで備わり（無料）、過去の利用に応じて自動で上がります。**事前に引き上げる（プリウォーミング）**ことで、急増の瞬間からスロットリングなしで受けられます。

ポイントを整理します。

- 課金モードを変えずに、**読み取り・書き込みのどちらか/両方**のウォームスループットだけを引き上げられる。
- 既存・新規どちらのテーブルでも可。Global Tables（2019.11.21）なら全レプリカに自動適用。
- **一度上げた値は下げられない**。プリウォーミング自体のリクエストには課金される（既定値の状態は無料）。

AWS SDK v3 での事前ウォーミング例（打ち上げ前夜に実行する運用スクリプト）：

```ts
import { DynamoDBClient, UpdateTableCommand } from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({});

/**
 * 打ち上げに備えて、毎秒「50,000読み取り・20,000書き込み」を即時にさばける状態へ温める。
 * 課金モード（オンデマンド/プロビジョンド）は変更しない。値は一度上げると下げられない点に注意。
 */
export async function preWarm(tableName: string): Promise<void> {
  await client.send(
    new UpdateTableCommand({
      TableName: tableName,
      WarmThroughput: {
        ReadUnitsPerSecond: 50_000,
        WriteUnitsPerSecond: 20_000,
      },
    }),
  );
}
```

判断基準はシンプルです。**「平常時の10倍以上のトラフィックが、特定の日時に来ると分かっている」ならプリウォーミング**。常に滑らかなサービスには不要です。

---

## 6. Auto Scaling の設計：滑らかな負荷を安く受ける

プロビジョンドを選ぶなら、**DynamoDB Auto Scaling（= AWS Application Auto Scaling）**はほぼ必須です。手動で容量を張り付けると、過剰確保（高コスト）か不足（スロットリング）のどちらかになります。

仕組みと公式の数値：

- **目標追跡（target tracking）**：消費容量が**目標使用率**に近づくよう、`UpdateTable` で確保容量を自動調整。目標使用率は**20〜90%**で設定可能。**定石は70%**。
- **スケールアップ**：消費が目標を**2分連続**で超えると発火。
- **スケールダウン**：**15データポイント連続**で目標を下回ると発火（＝下げには慎重。短い谷で容量を落として直後のスパイクで詰まるのを防ぐ）。
- **短時間スパイク**は容量変更ではなく、テーブル内蔵の**バーストキャパシティ**が吸収する。
- **GSIは別キャパシティ**。テーブルにAuto Scalingを入れるなら**GSIにも必ず同じ設定を**（公式が強く推奨）。新規GSIはバックフィル完了まで自動スケールが効かない点も注意。

Terraformでの本番構成（テーブル＋読み書き両方のスケーリングポリシー）：

```hcl
resource "aws_dynamodb_table" "app" {
  name         = "AppTable"
  billing_mode = "PROVISIONED"
  hash_key     = "PK"
  range_key    = "SK"

  read_capacity  = 5 # 下限。Auto Scaling が需要に応じて引き上げる
  write_capacity = 5

  attribute {
    name = "PK"
    type = "S"
  }
  attribute {
    name = "SK"
    type = "S"
  }

  # 期限切れアイテムを「無料」で自動削除（7章参照）
  ttl {
    attribute_name = "expiresAt"
    enabled        = true
  }

  point_in_time_recovery {
    enabled = true
  }
}

# --- 書き込みキャパシティの Auto Scaling ---
resource "aws_appautoscaling_target" "write" {
  service_namespace  = "dynamodb"
  resource_id        = "table/${aws_dynamodb_table.app.name}"
  scalable_dimension = "dynamodb:table:WriteCapacityUnits"
  min_capacity       = 5
  max_capacity       = 4000 # コストと事故の上限。負荷予測に合わせて調整
}

resource "aws_appautoscaling_policy" "write" {
  name               = "${aws_dynamodb_table.app.name}-write-target-tracking"
  service_namespace  = aws_appautoscaling_target.write.service_namespace
  resource_id        = aws_appautoscaling_target.write.resource_id
  scalable_dimension = aws_appautoscaling_target.write.scalable_dimension
  policy_type        = "TargetTrackingScaling"

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "DynamoDBWriteCapacityUtilization"
    }
    target_value = 70.0 # 目標使用率70%が定石
  }
}

# 読み取りも同じ要領で target/policy を定義する（predefined_metric_type は
# "DynamoDBReadCapacityUtilization"、scalable_dimension は ...:ReadCapacityUnits）。
# GSI を持つなら GSI 用にも同じ4リソースを必ず追加する。
```

ここでも限界を直視します。Auto Scalingは**CloudWatchアラーム経由で数分の遅れ**を伴い、`UpdateTable` 自体にも数分かかります。**急峻なスパイクには間に合わない**——だから短い山はバーストで、読めない山はオンデマンドで受けるのが正解です。「Auto Scalingがあるからスパイクも安心」は誤解です。

---

## 7. コスト最適化の実戦テクニック

ここまでの理解を、効く順に「効く施策」へ落とします。

### (1) TTLで「無料の」自動削除

DynamoDBの**TTL**は、アイテムごとに有効期限（**Unix エポック秒のNumber属性**）を持たせ、期限切れを**書き込みスループットを消費せずに自動削除**する仕組みです。公式が明言します。

> DynamoDB automatically deletes expired items within a few days of their expiration time, without consuming write throughput.（DynamoDBは期限切れアイテムを、書き込みスループットを消費せずに、期限から数日以内に自動削除する）

セッション・一時トークン・キャッシュ・ログのような**寿命のあるデータは、`DeleteItem`（有料）ではなくTTL（無料）で消す**のが鉄則。ストレージ料金も削減できます。ただし2点注意：

- **削除は「数日以内」で即時ではない**。期限切れだがまだ消えていないアイテムは読み取り・クエリ・スキャンに現れ得るので、**`FilterExpression` でアプリ側でも除外**する。
- TTL削除は**Streamsにサービス削除として流れ**、LSI/GSIからも消える。**Global Tablesでは、起点リージョンの削除はWCU無料だが、レプリカへの複製削除には複製WCU/書き込みユニットが課金**される。

### (2) Standard-IA テーブルクラスで「めったに読まないデータ」を安く

ストレージは多いがアクセスは稀（監査ログ、古い注文履歴など）なら、**Standard-IAテーブルクラス**でストレージ単価が **$0.25 → $0.10 / GB・月**に下がります（その代わり読み書き単価は上がる）。**ストレージ支配的でスループットが小さいテーブル**で効きます。

### (3) アイテムを小さく保つ（読み取りユニットの本丸）

[2章](#2-キャパシティの数え方を制す者がコストを制す)の通り、読み取りコストはアイテムサイズで決まります。属性名を短くし（`createdAt`→`ca` 等）、巨大BLOBはS3へ逃がして参照だけ持つ。**1アイテムは最大400KB**ですが、それは「上限」であって「目標」ではありません。

### (4) スパースGSIと投影属性の最小化

GSIは「投影した属性」の分だけストレージと書き込みコストを増やします。**必要な属性だけ投影**し、**該当アイテムだけにGSIキーを持たせる**（スパースインデックス）ことで、インデックスを小さく・安く保てます。設計の詳細は[シングルテーブル設計ガイド](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)へ。

### (5) Scanを設計から消す

本番のホットパスでの`Scan`は、テーブル全体を読んで課金される最大のアンチパターン。**アクセスパターンを先に確定し、Query（必要ならGSI）で取れる**キー設計にする。分析用途の全件処理は、DynamoDBを直接Scanするのではなく**S3へのエクスポート → Athena/Glue**に逃がすのが安く速い。

### (6) バックアップコストも設計対象

PITR（継続バックアップ）は **$0.20 / GB・月**、オンデマンドバックアップは **$0.10 / GB・月**。重要テーブルはPITRを入れる価値がありますが、**全テーブルに無条件で**は過剰になり得ます。保護対象を選別しましょう。

---

## 8. 可観測性：詰まる前に気づく

キャパシティ設計は「入れて終わり」ではなく、**観測して回す**ものです。最低限見るべきCloudWatchメトリクスと、鳴らすべきアラーム：

| メトリクス | 何を示すか | アクション |
| --- | --- | --- |
| `ThrottledRequests` / `ReadThrottleEvents` / `WriteThrottleEvents` | スロットリング発生 | **即アラート**。容量不足かホットキー |
| `ConsumedReadCapacityUnits` / `ConsumedWriteCapacityUnits` | 実消費量 | モード/容量の意思決定の根拠 |
| `OnlineIndexConsumedWriteCapacity` | GSIの消費 | GSIの容量不足検知 |
| `AccountProvisionedReadCapacityUtilization` 等 | アカウント上限への接近 | 増枠申請の判断 |

`ThrottledRequests` に対する最小限のアラーム（Terraform）：

```hcl
resource "aws_cloudwatch_metric_alarm" "ddb_throttle" {
  alarm_name          = "${aws_dynamodb_table.app.name}-throttled-requests"
  namespace           = "AWS/DynamoDB"
  metric_name         = "ThrottledRequests"
  dimensions          = { TableName = aws_dynamodb_table.app.name }
  statistic           = "Sum"
  period              = 60
  evaluation_periods  = 1
  threshold           = 0
  comparison_operator = "GreaterThanThreshold"
  treat_missing_data  = "notBreaching"
  alarm_actions       = [aws_sns_topic.alerts.arn] # Slack/PagerDuty へ
}
```

どのキーが詰まっているか分からないときは、**CloudWatch Contributor Insights for DynamoDB**を有効化すると、最も消費したパーティションキー（＝ホットキー）を特定できます。原因（コード）ではなく**症状（スロットリング）でアラートを鳴らし**、Contributor Insightsで犯人を辿る——この運用が止まらない基盤を作ります。

---

## 9. まとめ：本番テーブルのIaC（オンデマンド版）

最後に、ここまでの判断を凝縮した**「迷ったらこれ」**の本番テーブル定義を置きます。オンデマンドで始め、**最大スループットで請求暴走をガード**し、**TTLとPITR**を入れ、必要になったらGSIとプロビジョンドへ進化させる——という出発点です。

```hcl
resource "aws_dynamodb_table" "app" {
  name         = "AppTable"
  billing_mode = "PAY_PER_REQUEST" # オンデマンド = デフォルト推奨
  hash_key     = "PK"
  range_key    = "SK"

  attribute {
    name = "PK"
    type = "S"
  }
  attribute {
    name = "SK"
    type = "S"
  }

  # 事故・バグによる請求爆発を構造的に防ぐガードレール。
  # 想定ピークに余裕を持たせつつ、青天井にはしない。
  on_demand_throughput {
    max_read_request_units  = 50000
    max_write_request_units = 20000
  }

  # 寿命のあるデータは「無料」で自動削除（DeleteItem の WCU を払わない）
  ttl {
    attribute_name = "expiresAt"
    enabled        = true
  }

  point_in_time_recovery {
    enabled = true
  }

  # 削除保護：本番テーブルの誤削除を止める
  deletion_protection_enabled = true

  tags = {
    Environment = "production"
    CostCenter  = "platform"
  }
}
```

---

## FAQ

**Q. オンデマンドとプロビジョンド、結局どっちが安い？**
A. 確保した容量を**持続的に高稼働率（目安70%前後）で使い切れる定常負荷**ならプロビジョンド（＋リザーブド）が安く、**スパイク・予測不能・低稼働率なら**オンデマンドが安く・楽です。定価ではオンデマンドはプロビジョンド100%稼働換算の約3.46倍、損益分岐はおよそ稼働率30%。まず実測してから寄せましょう。

**Q. ホットパーティションとは？どう防ぐ？**
A. 特定のパーティションキーへアクセスが集中し、その1パーティションの上限（毎秒**3,000読み取り/1,000書き込みユニット**）に達してスロットリングする状態です。**高カーディナリティなキー設計**と、避けられない集中には**ライトシャーディング**（キーにサフィックスを付けて分散）で対処します。Adaptive Capacityが自動で緩和しますが、パーティション上限は超えられません。

**Q. Scanは使ってはいけない？**
A. 本番のオンラインパスでは原則NG。Scanは**返した分ではなく評価した分**（実質テーブル全体）で課金され、遅く高コストです。アクセスパターンを先に決め、Query/GSIで取れる設計に。全件分析はS3エクスポート＋Athenaへ。

**Q. ウォームスループットのプリウォーミングは必須？**
A. 普段は不要です。**特定日時に平常の10倍以上の急増が分かっている**（セール・ローンチ等）ときだけ、事前に温めて「前ピークの2倍／30分」制約を回避します。一度上げた値は下げられず、プリウォーミング操作には課金される点に注意。

**Q. あとから課金モードを変えられる？**
A. 変えられます。**プロビジョンド→オンデマンドは24時間で最大4回**、**オンデマンド→プロビジョンドはいつでも**。切り替えには数分かかり、その間は直前の容量に見合ったスループットで提供されます。

**Q. TTLで消したアイテムにすぐアクセスできなくなる？**
A. いいえ。削除は**期限から数日以内**で即時ではありません。期限切れだが未削除のアイテムは読み取りに現れ得るので、**`FilterExpression` でアプリ側でも除外**してください。削除自体は**WCU無料**です。

---

## おわりに：コストと性能は「設計」で決まる

DynamoDBの料金と速度は、運用中の節約術ではなく、**最初のキャパシティ設計**でほぼ決まります。

- **モードは負荷の形で選ぶ**：スパイク/予測不能はオンデマンド、定常/高稼働率はプロビジョンド＋リザーブド。
- **消費を実測する**：`ReturnConsumedCapacity` で数え、UpdateItem・条件失敗・Scan・FilterExpressionの「隠れ課金」を潰す。
- **パーティション上限を設計で守る**：3,000/1,000を高カーディナリティとライトシャーディングで分散し、Adaptive Capacityに頼り切らない。
- **無料の武器を使う**：TTL削除・バースト・Adaptive Capacity・ウォームスループットは正しく使えば強力。
- **観測して回す**：ThrottledRequestsで即アラート、Contributor Insightsでホットキーを特定。

正しさの設計（冪等性・原子性・整合性）は[DynamoDB シングルテーブル設計＆本番信頼性パターン完全ガイド](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)に、実際の決済基盤での適用は[サーバーレス決済基盤で「二重課金ゼロ」を設計する](/blog/dynamodb-payment-reliability-idempotency-zero-downtime)にまとめています。**速く・安く・安全に**DynamoDBを本番で稼がせる——その設計を、要件に合わせて一緒に詰めます。
