目次
- 基本的な考え方
- データモデリング
- パーティションキーの設計
- ソートキーの設計
- セカンダリインデックス
- シングルテーブル設計
- アイテムサイズと属性
- クエリとスキャン
- 書き込みと整合性
- キャパシティとコスト
- TTL・Streams・イベント駆動
- セキュリティ
- 運用・監視・バックアップ
- SDK・クライアント側の実装
- アンチパターン集
- 設計チェックリスト
1. 基本的な考え方
RDBとDynamoDBでは設計の出発点が逆になる。
| 観点 | RDB | DynamoDB |
|---|---|---|
| 設計の起点 | エンティティと正規化 | アクセスパターン |
| 結合 | JOINで実行時に結合 | 事前に結合済みの形で保存(非正規化) |
| クエリの柔軟性 | 高い(後から自由にSQL) | 低い(設計時に想定したもののみ効率的) |
| スケール | 垂直スケール中心 | パーティションによる水平スケール |
原則
- 「どんなデータがあるか」ではなく「どう読み書きするか」から設計する
- アクセスパターンを洗い出す前にテーブルを作らない
- 1回のリクエストで必要なデータが取れる形を目指す(N+1を作らない)
- 正規化よりも、読み取り効率のための意図的な非正規化・重複を許容する
2. データモデリング
設計手順
- ユースケースとアクセスパターンを列挙する
- 例:「ユーザーIDでユーザー取得」「ユーザーの注文を新しい順に20件取得」「ステータス別の注文一覧」
- 各パターンの頻度・レイテンシ要件・データ量を見積もる
- エンティティ関係図(ER図)を書き、1対多・多対多を把握する
- 主キー(PK/SK)とGSIで各パターンを満たす設計を作る
- アクセスパターンごとに「どのテーブル/インデックスに、どのキー条件で問い合わせるか」を表にする
アクセスパターン表の例
| # | アクセスパターン | 対象 | キー条件 |
|---|---|---|---|
| 1 | ユーザー取得 | テーブル | PK = USER#<id>, SK = PROFILE |
| 2 | ユーザーの注文一覧(新しい順) | テーブル | PK = USER#<id>, SK begins_with ORDER#(降順) |
| 3 | 注文IDで注文取得 | GSI1 | GSI1PK = ORDER#<orderId> |
| 4 | ステータス別注文 | GSI2 | GSI2PK = STATUS#<status>, GSI2SK = <date> |
リレーションの表現
- 1対多:親のIDをパーティションキーにし、子をソートキーで並べる(アイテムコレクション)
- 多対多:隣接リスト(Adjacency List)パターン。両方向の関係をアイテムとして持ち、GSIでキーを反転させる
- 小さく変化の少ない子データ:親アイテム内にリスト/マップとして埋め込む(ただしサイズ上限に注意)
3. パーティションキーの設計
パーティションキーはデータの分散とスループットを決める最重要要素。
良いパーティションキーの条件
- カーディナリティが高い(値の種類が多い):ユーザーID、デバイスID、注文IDなど
- アクセスが均等に分散する:特定の値にリクエストが集中しない
- アクセスパターンで必ず値が分かっている
避けるべきパーティションキー
- ステータス(
ACTIVE/INACTIVE)、性別、日付だけ、など種類が少ないもの - 時系列で「今日」に書き込みが集中するキー
ホットパーティション対策
- 1パーティションあたりの上限目安:読み取り 3,000 RCU/秒、書き込み 1,000 WCU/秒
- 書き込みシャーディング:キーにサフィックスを付けて分散する
- 例:
DATE#2026-11-02#<0〜9のランダム値> - 読み取り時は全シャードに並列でQueryし、アプリ側でマージする
- 例:
- 計算可能なサフィックス:IDのハッシュ値からシャード番号を決めると、特定アイテムの読み取り時にシャードを特定できる
- アダプティブキャパシティは偏りを自動で緩和するが、単一キーへの上限を超える集中は解決しないため、設計で回避する
4. ソートキーの設計
活用方法
- 範囲検索:
begins_with、between、>,<で絞り込める - 並び順:ソートキー順に格納されるため、
ScanIndexForward=falseで降順取得が可能 - 階層構造の表現:
COUNTRY#JP#PREF#TOKYO#CITY#SHIBUYAのように連結すると、任意の階層で前方一致検索できる
Tips
- 日時は ISO 8601 形式(
2026-11-02T10:00:00Z)にすると文字列ソートで時系列順になる - 時系列で一意性が必要なら ULID や「タイムスタンプ+ID」を使う
- 数値を文字列で持つ場合はゼロ埋め(
0000123)しないとソート順が崩れる - エンティティ種別をプレフィックスにする(
ORDER#,ITEM#)と、同一パーティション内で種類別に取得できる - バージョン管理:
v0(最新)とv1, v2...(履歴)を持つパターンで、最新取得と履歴取得を両立できる
5. セカンダリインデックス
GSI と LSI の比較
| 項目 | GSI(グローバル) | LSI(ローカル) |
|---|---|---|
| キー | 任意のPK/SK | テーブルと同じPK、別のSK |
| 作成タイミング | いつでも追加・削除可 | テーブル作成時のみ |
| 整合性 | 結果整合性のみ | 強い整合性も可 |
| キャパシティ | テーブルとは別 | テーブルと共有 |
| サイズ制限 | なし | アイテムコレクションあたり 10GB |
| 上限数 | テーブルあたり20(デフォルト) | テーブルあたり5 |
基本はGSIを使う。 LSIは強い整合性が必須で、10GB制限に収まることが確実な場合のみ検討する。
GSI のベストプラクティス
- 射影(Projection)を最小限に:
KEYS_ONLY→INCLUDE→ALLの順にコストが増える。必要な属性だけを射影する - スパースインデックス:GSIのキー属性を持つアイテムだけがインデックスされる性質を利用する
- 例:未処理の注文だけ
PendingFlagを持たせ、GSIで未処理一覧を効率的に取得。処理後は属性を削除
- 例:未処理の注文だけ
- GSIオーバーロード:
GSI1PK/GSI1SKのような汎用名にして、エンティティごとに異なる意味の値を入れ、1つのGSIで複数パターンを賄う - GSIのスループット不足はベーステーブルの書き込みをスロットリングさせる。GSIのキャパシティも十分に確保する
- GSIのパーティションキーにもカーディナリティと分散の原則が適用される
6. シングルテーブル設計
関連する複数のエンティティを1つのテーブルに格納する手法。
メリット
- 関連データを1回のQueryでまとめて取得できる(擬似JOIN)
- テーブル数・アラーム・バックアップ設定などの運用対象が減る
- キャパシティをテーブル全体で効率的に共有できる
デメリット
- 学習コストが高く、データの可読性が落ちる
- 新しいアクセスパターンの追加が難しい場合がある
- 分析用途へのエクスポート時に変換が必要
- エンティティごとに異なるストリーム処理・バックアップ方針を取りにくい
判断基準
| シングルテーブルが向く | マルチテーブルが向く |
|---|---|
| アクセスパターンが明確で安定している | 要件が流動的・探索的 |
| 関連データを同時に取得するケースが多い | エンティティ同士の関連が薄い |
| 高いパフォーマンスが必要 | チームの学習コストを抑えたい |
| GraphQLのリゾルバのようにエンティティ単位で取得する |
「必ずシングルテーブルにすべき」ではない。 一緒に取得するデータは同じテーブルにまとめ、無関係なものは分ける、という程度のバランスで考える。
実装上の慣習
- キー属性は
PK/SK/GSI1PK/GSI1SKのように汎用名にする - 各アイテムに
Type(EntityType)属性を持たせ、可読性とフィルタリングを補助する - ElectroDB や DynamoDB Toolbox などのライブラリでキー設計の抽象化を検討する
7. アイテムサイズと属性
- アイテムサイズの上限は 400KB(属性名+値の合計)
- 料金・キャパシティはサイズに比例する
- 読み取り:4KB単位(強い整合性で1 RCU、結果整合性で0.5 RCU)
- 書き込み:1KB単位で1 WCU
- 属性名を短くする:属性名もサイズに含まれる(大量アイテムでは効果大)
- 大きなデータはS3へ:画像・ドキュメント・大きなJSONはS3に置き、DynamoDBにはS3キーとメタデータのみ保存
- 圧縮:大きなテキストはgzip等で圧縮してBinary型で保存する(ただしフィルタや条件式では使えなくなる)
- 垂直分割:頻繁に更新する属性と滅多に変わらない大きな属性を別アイテムに分けると、更新時のWCUを削減できる
- 無制限に増えるリスト(コメント一覧など)を1アイテムに埋め込まない。別アイテムに分ける
8. クエリとスキャン
Query を基本にする
Queryはパーティションキー指定でピンポイントに読むため効率的Scanはテーブル全体を読み、全アイテム分のRCUを消費する
Scan を使ってよいケース
- 小さなテーブル、管理用の一括処理、データ移行
- 使う場合は以下を検討する
- 並列スキャン(
Segment/TotalSegments)で高速化 Limitとページングでスループット消費をならす- 本番トラフィック用テーブルへの影響を避ける(オフピーク実行、レート制御)
- 分析目的なら S3エクスポート+Athena や ゼロETL統合を優先する
- 並列スキャン(
FilterExpression の注意点
- フィルタは読み取り後に適用されるため、RCUは削減されない
- フィルタで大部分を捨てる設計なら、キー設計かGSIで絞り込めるように見直す
Limitはフィルタ前の件数に適用される。結果が0件でもLastEvaluatedKeyがある場合は続きを読む必要がある
その他
- ProjectionExpression で必要な属性だけを取得する(転送量削減。RCUは変わらない点に注意)
- ページング:
LastEvaluatedKeyを使う。1回のレスポンスは最大1MB - 必ずキー条件式で絞り込み、
begins_with/betweenを活用する
9. 書き込みと整合性
読み取り整合性
- デフォルトは結果整合性(低コスト)。書き込み直後の読み取りが必須な場合のみ強い整合性を使う
- GSIは結果整合性のみ。書き込み直後にGSIを読む処理は、反映遅延を前提に設計する
条件付き書き込み
ConditionExpressionで楽観的ロックを実装する- 例:
version属性を持たせ、version = :expectedを条件に更新
- 例:
- 重複作成の防止:
attribute_not_exists(PK)を条件にPutItem - 失敗時(
ConditionalCheckFailedException)にどう扱うかをアプリ側で明確にする
アトミック操作
- カウンタは
UpdateItemのADDやSET count = count + :incで更新する(読み取り→書き込みをしない) - ただし冪等ではないため、リトライで二重加算されうる。厳密さが必要ならトランザクションや冪等キーを使う
トランザクション
TransactWriteItems/TransactGetItemsで最大100アイテムをACIDに操作できる- 通常操作の2倍のキャパシティを消費するため、必要な場面に限定する
ClientRequestTokenで冪等性を確保する- 一意制約(メールアドレスの重複禁止など)は、一意値をキーにした別アイテムをトランザクションで同時作成して実現する
バッチ操作
BatchWriteItem(最大25件)/BatchGetItem(最大100件)でリクエスト回数を削減するUnprocessedItems/UnprocessedKeysを必ず処理する(指数バックオフでリトライ)- バッチは原子的ではない。一部失敗がありうる
10. キャパシティとコスト
キャパシティモードの選択
| モード | 向いているケース |
|---|---|
| オンデマンド | 新規サービス、トラフィックが読めない、スパイクが大きい、アイドル時間が長い |
| プロビジョンド+Auto Scaling | トラフィックが安定・予測可能で、コストを最適化したい |
| プロビジョンド+リザーブドキャパシティ | 長期的に安定した高いベースライン負荷がある |
- まずオンデマンドで始め、トラフィック傾向が見えたらプロビジョンドへの切り替えを検討するのが定石
- オンデマンドでも最大スループットを設定でき、コストの暴走を防げる
- Auto Scalingは急激なスパイクには追従が遅れる。スパイクが予見できるなら事前にスケールアップする
テーブルクラス
- Standard:一般的な用途
- Standard-IA:ストレージコストが支配的(アクセス頻度の低い大量データ)なテーブル向け。読み書き単価は上がる
コスト削減のポイント
- アイテムサイズを小さくする(属性名短縮、S3オフロード、圧縮)
- GSIの射影を最小限にし、不要なGSIを削除する
- 結果整合性読み取りを使う
- TTLで不要データを自動削除する(TTLによる削除はWCUを消費しない)
- Scanを避ける
- 読み取りが非常に多いなら DAX やアプリ側キャッシュを検討する
11. TTL・Streams・イベント駆動
TTL(Time To Live)
- エポック秒(Number型)の属性を指定すると、期限切れアイテムを自動削除する
- 削除は即時ではない(期限後しばらく残ることがある)。読み取り時に期限をフィルタで除外する
- セッション、一時トークン、ログ、キャッシュなどに最適
- TTL削除はStreamsに流れるため、アーカイブ(S3退避など)のトリガーにできる
DynamoDB Streams
- アイテムの変更を時系列で取得できる(24時間保持)
- 用途例
- 集計値の非同期更新(マテリアライズドビュー)
- OpenSearchなど検索エンジンへの同期
- 他サービスへのイベント通知
- 監査ログ
- Lambdaのイベントソースマッピングでは以下を設定する
- バッチサイズ・バッチウィンドウ
- 失敗時の送信先(DLQ / オンフェイラー宛先)
BisectBatchOnFunctionError(バッチ分割リトライ)- 最大リトライ回数・最大レコード経過時間(ポイズンメッセージでシャードが詰まるのを防ぐ)
- イベントフィルタリングで不要なイベントを除外
- コンシューマ処理は冪等に実装する(同じレコードが複数回届く可能性がある)
- より長い保持期間やファンアウトが必要なら Kinesis Data Streams 連携を検討する
12. セキュリティ
- 最小権限のIAM:テーブル・インデックス単位でアクションを絞る
- きめ細かなアクセス制御:
dynamodb:LeadingKeys条件キーで、ユーザーが自分のパーティションだけにアクセスするよう制限できる - 暗号化:保存時暗号化は常に有効。要件に応じてカスタマーマネージドKMSキーを使う
- VPCエンドポイント(ゲートウェイ型)で、インターネットを経由せずにアクセスする
- リソースベースポリシーでクロスアカウントアクセスを制御する
- CloudTrail でコントロールプレーン操作を記録し、必要に応じてデータプレーンイベントも有効化する
- 誤削除対策として削除保護(Deletion Protection) を有効にする
- 機密データ(個人情報など)はクライアント側暗号化(AWS Database Encryption SDK)も検討する
13. 運用・監視・バックアップ
監視すべき CloudWatch メトリクス
| メトリクス | 見るポイント |
|---|---|
ThrottledRequests / ReadThrottleEvents / WriteThrottleEvents | スロットリングの発生 |
ConsumedReadCapacityUnits / ConsumedWriteCapacityUnits | 消費量とプロビジョン量の比較 |
SuccessfulRequestLatency | レイテンシの悪化 |
SystemErrors / UserErrors | エラー傾向 |
OnlineIndexPercentageProgress | GSI作成の進捗 |
ReplicationLatency | グローバルテーブルの複製遅延 |
- CloudWatch Contributor Insights を有効にすると、アクセスの多いキー(ホットキー)を特定できる
- スロットリングのアラームを必ず設定する
バックアップと復旧
- PITR(ポイントインタイムリカバリ) を有効化する(保持期間内の任意時点へ復元可能)
- AWS Backup で長期保持・クロスリージョン/クロスアカウントコピーを管理する
- 復元は新しいテーブルとして作成される。復元手順(IAM、Auto Scaling、Streams、TTLなどの再設定)を事前に手順化・訓練しておく
可用性
- 単一リージョン内でマルチAZ冗長は標準
- リージョン障害への備えやグローバルな低レイテンシが必要ならグローバルテーブルを使う
- マルチリージョン書き込み時の競合解決(最後の書き込みが優先) を理解した上で設計する
IaC
- テーブル定義は CloudFormation / CDK / Terraform で管理する
- 本番テーブルには
DeletionPolicy: Retain等を設定し、スタック削除時の消失を防ぐ
14. SDK・クライアント側の実装
- SDKの自動リトライ(指数バックオフ+ジッター)を有効にし、リトライ回数を適切に設定する
- タイムアウト(接続・リクエスト)を明示的に設定する。デフォルトは長すぎることが多い
- クライアントを再利用する(Lambdaではハンドラ外で初期化し、コネクションを使い回す)
- HTTP Keep-Alive を有効にする
- 高レベルAPI(DocumentClient、Enhanced Client、PynamoDBなど)で型変換の手間を減らす
- 予約語との衝突を避けるため、
ExpressionAttributeNames(#name)を使う - 値は必ず
ExpressionAttributeValuesで渡す(文字列連結で式を組み立てない) - ローカル開発・テストには DynamoDB Local や LocalStack を使う
15. アンチパターン集
| アンチパターン | 問題 | 対策 |
|---|---|---|
| RDBのテーブル設計をそのまま移植 | JOINできず大量のリクエストが発生 | アクセスパターン起点で再設計 |
| 低カーディナリティのパーティションキー | ホットパーティション | 高カーディナリティなキー、書き込みシャーディング |
| Scanを日常的なクエリに使う | 高コスト・低速 | Query+GSIで設計 |
| FilterExpressionで絞り込みを担う | RCUを無駄に消費 | キー条件で絞れるようにする |
GSIの射影を常に ALL | ストレージ・書き込みコスト増 | 必要な属性のみ射影 |
| 無制限に増えるリストを1アイテムに埋め込む | 400KB上限、更新コスト増 | 別アイテムに分割 |
UnprocessedItems を無視 | データ欠損 | バックオフ付きリトライ |
| 読み取り→計算→書き込みでカウンタ更新 | 競合で値が壊れる | アトミック更新・条件付き書き込み |
| GSIを強い整合性前提で読む | 書き込み直後に古い値を返す | 結果整合性を前提に設計 |
| 大きなバイナリを直接保存 | コスト増、サイズ上限 | S3に保存しキーのみ格納 |
| 分析クエリを本番テーブルに直接実行 | 本番トラフィックへの影響 | S3エクスポート、ゼロETL統合 |
16. 設計チェックリスト
設計時
- アクセスパターンをすべて列挙し、頻度とデータ量を見積もった
- 各アクセスパターンがQuery(またはGetItem)で実現できる
- パーティションキーのカーディナリティが高く、アクセスが分散する
- ホットキーになりうる箇所にシャーディング等の対策がある
- GSIの射影は必要最小限になっている
- アイテムサイズが400KBに達しない設計になっている
- 一意制約・同時更新に対する整合性の担保方法が決まっている
- 不要データの削除方法(TTL等)が決まっている
実装時
- SDKのリトライとタイムアウトを設定した
- バッチ操作の未処理アイテムをリトライしている
- 条件付き書き込みの失敗をハンドリングしている
- Streamsコンシューマが冪等で、失敗時の退避先がある
運用時
- PITRと削除保護を有効化した
- スロットリング・エラーのアラームを設定した
- Contributor Insightsでホットキーを確認できる
- キャパシティモードがトラフィック特性に合っている
- IAMが最小権限になっている
- 復元手順をドキュメント化し、訓練した