こんにちは!株式会社GROWTH VERSEに所属しております川田剣士です。最近DynamoDBを業務で設計する機会があったため、個人的に学習してDynamoDBの知見を整理し技術記事で体系的にまとめようと思いました。記事に対する質問・コメント大歓迎です!
はじめに
DynamoDBはAWSを使っていると必ずと言っていいほど選択肢に挙がるデータベースですが、RDB(MySQL/PostgreSQLなど)に慣れているエンジニアほど、次のような違和感を持つことが多いのではないでしょうか。
- テーブル設計のやり方が分からない
- 正規化しなくていいと言われるが、何を基準に設計すればいいのか不安
- GSIを追加するとコストが増えると聞いて怖い
- とりあえず使ってみるができない
私自身、RDBを前提にした設計や実装を長く行ってきた中でDynamoDBの設計思想を学ぶうちに、「これはRDBの延長線で考えると確実に失敗するな」と感じました。DynamoDB が難しく感じられる最大の理由は、機能が多いからでも、AWS特有の概念が多いからでもありません。
設計の出発点が、RDBと根本的に異なるから
そしてその違いが、コストに直結するから
です。RDBでは、まずエンティティを整理し、正規化します。クエリは後から考えるものであり、JOINによって柔軟にデータを取得できます。一方DynamoDBでは、JOINは存在せずクエリの自由度は低く「どんなアクセスをするか」から設計が始まります。
つまりDynamoDBでは、テーブル設計=アプリケーション設計=コスト設計と言っても過言ではありません。この記事では、RDB への理解があるエンジニアを対象に
- どんなケースでDynamoDBを選ぶべきなのか
- テーブル設計をどのような手順で考えるのか
- その設計がコストにどう影響するのか
といった点を、概念・設計思想を中心に解説します。この記事を読み終えたときに、DynamoDBを怖いNoSQLではなく、思想が明確なデータベースとして理解でき、「選定して良いケース/悪いケース」を説明できる状態になることを目標としています。
1. DynamoDBとは何か(RDBとの思想の違い)
DynamoDBは、AWSが提供するフルマネージドなNoSQLデータベースです。一般的にはKey-Value/Wide Columnストア(行ごとに持っているカラムの集合が違ってもよいデータモデル)に分類され、高いスケーラビリティと安定したレイテンシを特徴としています。ただし、DynamoDB を正しく理解するために重要なのは、「何ができるか」よりも「何を前提に作られているか」です。
RDBはデータの整合性を中心に設計されている
RDB は、次のような思想を前提に設計されています。
- 正規化されたテーブル構造
- 外部キーによるリレーション
- JOIN による柔軟なデータ取得
- クエリの自由度を高めるためのインデックス設計
このためRDBでは、
- エンティティを整理する
- 正規化する
- クエリは後から考える
という設計手順が自然に成立します。多少クエリが非効率でも、インデックスを追加したり、SQLを工夫したりすることで後から最適化できる余地があるのもRDBの強みです。
DynamoDBはスケールとレイテンシを最優先に設計されている
一方DynamoDBは、まったく異なる前提で設計されています。
- ペタバイト級のデータ
- 秒間数万〜数十万リクエスト
- 常に低レイテンシ
これらを実現するため、DynamoDB では意図的に多くの機能が削ぎ落とされています。
- JOIN は存在しない
- クエリ条件はキーに強く依存する
- 集計や柔軟な検索は苦手
その代わりに、どのキーでアクセスされるかが明確であれば非常に高いスループットと安定性を提供できるという設計になっています。そのため、テーブル作成時点で、
- パーティションキー(PK)
- ソートキー(SK)
- GSI(Global Secondary Index)
といったキー構造は厳密に設計する必要があります。
設計の出発点が根本的に異なる
RDBとDynamoDBの最大の違いは、設計の出発点にあります。
| 観点 | RDB | DynamoDB |
|---|---|---|
| 設計の起点 | データ構造 | アクセスパターン |
| 正規化 | 前提 | 原則しない |
| JOIN | 可能 | 不可 |
| クエリの自由度 | 高い | 低い |
| スケーリング | 要設計 | 自動 |
DynamoDBでは、
- どんなクエリを投げるか
- そのクエリはどれくらいの頻度か
を最初に洗い出さなければ、テーブル設計ができません。この点を理解せずにRDBと同じ感覚で設計すると、
- 効率の悪いScanが多発する
- GSIが不必要に増える
- コストが予想以上に膨らむ
といった問題が起こりがちです。RDBでは、データベースはアプリケーションから比較的独立した存在として扱えます。しかしDynamoDBでは、アプリケーションのアクセスパターンがそのままテーブル設計に反映されます。そのためDynamoDBは汎用的なデータベースではなく特定のユースケースに最適化されたデータベースとして捉えるのが適切です。
DynamoDBを選定すべきケース
アクセスパターンが明確で、種類が少ない
DynamoDBは、どのキーで、どの条件でアクセスされるかが明確な場合に最も力を発揮します。例えば、
- ユーザーIDでデータを取得する
- 特定のID配下の一覧を時系列で取得する
- 最新N件だけを取得する
といった決まった形のクエリが中心のシステムです。RDBのように、
- 画面追加のたびに WHERE 条件が増える
- 管理画面で自由検索をしたい
といった要件が強い場合は、DynamoDBとの相性は良くありません。
高トラフィック・スパイクが発生する
DynamoDBはスケールアウトを前提とした設計になっており、
- 急激なアクセス増加
- トラフィックの波が大きい
といったケースでも、アプリケーション側で特別な対応をせずに耐えられるのが大きな強みです。特に、イベントやキャンペーン時に負荷が跳ね上がるといったシステムでは、RDBよりも運用コストを下げられることがあります。
スケーリングや運用をDBに任せたい
RDBでは、
- シャーディング
- リードレプリカ
- フェイルオーバー
など、スケールや可用性を考慮した設計・運用が必要になります。DynamoDB ではこれらがマネージドで提供されるため、
- インフラ運用を極力減らしたい
- DBのスケーリングを考えたくない
という場合に向いています。
正確な集計より高速な参照が重要
DynamoDBは集計や複雑な検索が得意ではありません。その代わり、
- 単一アイテム取得
- 条件が限定された一覧取得
といった操作を常に安定したレイテンシで実行できる点が強みです。ログイン状態の取得、設定情報の参照など、参照性能が最優先されるデータには非常に向いています。DynamoDBはAP型の分散システムであるため、結果整合性型のシステムである一方、高速なデータ抽出に適しています。
DynamoDBを避けた方がよいケース
複雑な検索・集計が多い
次のような要件がある場合、DynamoDBは扱いづらくなります。
- 複数条件による自由検索
- 管理画面での柔軟な検索
これらはRDBが最も得意とする領域です。DynamoDBで無理に実現しようとすると、
- Scanが多発する
- GSIが増え続ける
- コストが膨らむ
といった問題に繋がります。
アクセスパターンが頻繁に変わる
DynamoDBのテーブル設計は、最初に想定したアクセスパターンに強く依存します。そのため、
- 仕様変更が頻繁
- 将来の要件が読めない
といった場合、設計変更(キーの追加などを指している)のコストが高くなりがちです。RDBのように「とりあえず列を追加して対応する」という逃げ道はあまりありません。
RDB+DynamoDBの併用という選択肢
重要なのは、RDBかDynamoDBかどちらか一方に寄せる必要はないという点です。例えば、
- マスタデータ・管理画面 → RDB
- 高頻度参照・セッション情報 → DynamoDB
といった役割分担は、実務でもよく使われます。DynamoDBはRDBを置き換える存在ではなく、補完する存在として捉える方が、無理のない設計になります。
RDB思考からの脱却:DynamoDB設計の考え方
DynamoDB を難しく感じる最大の理由は、RDBで身についた設計手順が、そのまま使えないことにあります。RDBでは、設計の良し悪しは
- 正規化されているか
- 重複がないか
といった観点で評価されがちです。しかし DynamoDBでは、それらは必ずしも正解ではありません。
テーブル設計の前に考えるべきこと
DynamoDBの設計は、テーブル定義から始めてはいけません。まず最初にやるべきなのは、どんなクエリが、どのくらいの頻度で実行されるかを洗い出すことです。具体的には、次のような問いに答える必要があります。
- どのIDを使って取得するのか
- 単一取得か、一覧取得か
- 並び順は必要か
- 最新何件を取るのか
- そのクエリはどれくらいの頻度で呼ばれるのか
これらが曖昧なままテーブルを作ると、後から設計を修正するのが非常に難しくなります。
ドメインモデル図ではなくクエリ一覧から始める
RDBではドメインモデル図が設計の中心として機能しますが、DynamoDBではドメインモデル図はほとんど役に立ちません。代わりに作るべきなのは、アプリケーションが発行するクエリの一覧です。例えば、
- ユーザーIDからユーザー情報を取得
- ユーザーID配下の注文一覧を取得(作成日時順)
- 注文IDから注文詳細を取得
といった形で、APIレベルでのアクセスパターンを書き出します。この時点で、
- このクエリはQuery(PKの完全一致+SK条件で範囲取得)で実行できるか
- GetItem(PK+SKを完全一致で指定して1件取得。最速・最安)で済ませられないか
などの実行方法を意識することが重要です。DynamoDB設計でよく使われる言葉にアクセスパターン駆動設計があります。これは、データ構造を先に決めるのではなく、アクセス方法に合わせてデータ構造を決めるという考え方です。例えば、
- ユーザーIDで必ず取得する → PK にユーザーIDを使う
- 時系列で並べたい → SK に時刻を含める
- 一覧取得が多い → 同一パーティションに集める
といった具合です。
DynamoDB設計でよくある失敗例
RDBと同じ感覚でテーブルを分ける
- usersテーブル
- ordersテーブル
- order_itemsテーブル
といったRDB的な分割をすると、アプリケーション側で何度もGetItem/Queryを呼ぶことになりがちです。結果として、
- レイテンシが悪化する
- コストが増える
という問題が起こります。
後からGSIを足せばいいと考える
GSIは非常に便利ですが、
- ストレージコスト
- 書き込みコスト
が確実に増えます。また、GSIはアクセスパターンの増加そのものを意味するため、無計画に追加するとテーブル設計が破綻しやすくなります。
Scanを前提にしてしまう
Scan(テーブルまたはGSI全体を読む)は使えなくはありませんが、データ量に比例してコストが増えレイテンシも不安定になります。Scanが必要になる設計は、アクセスパターンの洗い出しが不十分なサインだと考えた方が安全です。
DynamoDBの基本構成要素
ここまでで、DynamoDBはアクセスパターンから設計するDBであることを見てきました。この章では、その設計を支える最低限理解しておくべき構成要素を整理します。
パーティションキー(Partition Key)
パーティションキーは、DynamoDBの最も重要な要素です。
- データの格納先を決める
- クエリの起点になる
- スケーラビリティに直結する
という役割を持っています。同じパーティションキーを持つアイテムは、論理的に同じグループとして扱われます。そのため、
- 一覧取得したいデータ
- まとめて取得したいデータ
は、基本的に同じパーティションキーを持たせます。
ソートキー(Sort Key)
ソートキーは、同じパーティションキー内での並び順を決めるキーです。
- 時系列データ
- バージョン管理
- 種別ごとのデータ分離
などに使われます。ソートキーを持つテーブルでは、
- パーティションキー→等価条件
- ソートキー→範囲条件
というクエリが可能になります。これにより、
- 最新N件取得
- 期間指定の一覧取得
といった操作を、Scanなしで実現できます。ソートキーはアイテムの一意識別子の一部であり、その値を更新することはできないため、変更が必要な場合は削除と再作成を行うというフローが発生します。
アイテムと属性
DynamoDBにおける1行分のデータがアイテムです。アイテムは、
- 属性を自由に持てる
- 属性数や型は固定されない
という特徴があります。ただし注意点として、
- 1アイテムの最大サイズは400KB
- アイテムサイズはそのままコストに影響
します。不要な属性を詰め込みすぎると、読み取りコストが増える点には注意が必要です。
GetItem/Query/Scanの違い
DynamoDBでは、どのAPIを使うかでコストと性能が大きく変わります。
- GetItem:PK(+SK)を指定した単一取得
- 最も高速・低コスト
- Query:PKを指定した複数件取得
- ソートキー条件が使える
- Scan:テーブル全体を走査
- コスト・レイテンシともに高い
基本方針としては、GetItem/Queryだけで要件を満たせる設計を目指すべきです。
強い一貫性と結果整合性
DynamoDBでは、読み取り時に一貫性モデルを選択できます。
- 結果整合性(デフォルト)
- 強い一貫性
結果整合性は、
- レイテンシが低い
- コストが安い
というメリットがあります。一方、直前の書き込みを必ず反映したい場合は、強い一貫性を選択します。ただし強い一貫性は、読み取りコストが2倍といったデメリットがあります。
GSI(Global Secondary Index)
GSIは、任意の属性をキーとして指定することでQuery実行を可能にする仕組みです。
- メインテーブルとは異なるPK/SK
- 複数作成可能
という特徴があります。一見すると便利ですが、
- 書き込み時にGSIにも反映される
- ストレージコストが増える
ため、多用するとコストと設計が複雑になります。GSIは、
- 最初の設計でどうしても必要なものだけを作る
という姿勢が重要です。
DynamoDBの構成要素は「設計の制約」
ここで紹介した要素は、単なる機能ではなく設計上の制約です。
- パーティションキーをどう切るか
- ソートキーに何を入れるか
- GSIをどこまで許容するか
これらはすべて、
- アクセスパターン
- コスト
- 将来の拡張性
に直結します。
テーブル設計の基本パターン
ここまでで、DynamoDBはアクセスパターンに最適化されたテーブルを作るDBであることを見てきました。次は、実務でよく使われるテーブル設計の基本パターンを整理します。
単一テーブル設計という考え方
DynamoDBの設計でよく耳にするのが単一テーブル設計(Single Table Design)です。これは、複数のエンティティを、1つのテーブルにまとめて格納するという設計方針です。RDBに慣れていると違和感がありますが、DynamoDBではこちらの方が自然なケースが多くあります。
なぜ単一テーブル設計を採用するのか
理由はシンプルです。
- JOINができない
- Queryは同一パーティション内でしか使えない
という制約があるため、一緒に取得したいデータは、最初から一緒に置く必要があるからです。テーブルを分けた場合、分けたテーブル分のリクエストが必要になってしまいますが、一つにまとめることで一度に取得が可能になります。例えば、
- ユーザー情報
- ユーザー配下の注文
- 注文の明細
を1回のQueryで取得したい場合、これらは同じパーティションキーを持つ必要があります。
エンティティを区別する方法
単一テーブル設計では、異なるエンティティをどう区別するかが重要になります。一般的には、
- PKで親の概念を表す
- SKでエンティティ種別+識別子を表す
といった方法がよく使われます。例えば、
- PK = USER#123
- SK = PROFILE
- SK = ORDER#20240101
- SK = ORDER#20240102
このようにすると、ユーザー情報ろユーザーの注文一覧を1回のQueryで取得できます。
時系列データの設計パターン
DynamoDB では、時系列データの設計が頻出します。この場合、
- PK:集約単位(ユーザーIDなど)
- SK:時刻を含む文字列 or 数値
とするのが基本です。例えば、
- SK = 2024-01-01T10:00:00
- SK = ORDER#2024-01-01T10:00:00
のように設計すると、
- 最新N件取得
- 期間指定取得
がQueryで実現できます。
複数アクセスパターンをどう扱うか
1つのテーブルで複数のアクセスパターンを満たしたい場合、
- PK / SK の設計を工夫する
- それでも無理なら GSI を使う
という順番で考えます。ここで重要なのは、GSIは明確な理由がある場合にだけ使うという姿勢です。GSIを使うということは、そのアクセスパターンを追加のコストを払って維持するという意味になります。
テーブル設計はクエリを成立させるための構造
DynamoDBのテーブル設計は、
- データをきれいに整理するため
- 将来の柔軟性を確保するため
ではなく、クエリを、Query/GetItemで成立させるために行います。そのため、
- RDB的に美しくない
- 重複が多い
と感じる構造でも、DynamoDBでは正解であることが多いです。
DynamoDBのコスト構造
DynamoDBで課金される主な要素
DynamoDBのコスト要素は、大きく次の4つです。
- 読み取りキャパシティ(RCU)
- 書き込みキャパシティ(WCU)
- ストレージ
- GSI(追加インデックス)のコスト
重要なのは、クエリの書き方とテーブル設計がこれらすべてに影響するという点です。
読み取りコスト(RCU)の考え方
基本ルール
- 1RCU→4KBまでのアイテムを強い一貫性で1回読み取れる
- 結果整合性なら0.5RCU
つまり、読み込み件数ではなく、アイテムサイズが大きいほど線形にコストが増大します。
GetItemとQueryの違い
- GetItem
- 単一アイテム取得
- 最小コスト・最小レイテンシ
- Query
- 複数アイテム取得
- 合計取得サイズに比例してRCU消費
Scanは、条件に合うかどうかに関係なくテーブル(またはインデックス)全体を読むため、データ量が増えるほど、確実にコストとレイテンシが悪化します。Scanが必要になる設計はコスト面で黄色信号です。また、ProjectionExpressionをリクエストごとに指定することができますが、こちらを使用すると、アイテムに保存されている属性のうち、必要な属性だけを読み取ることができるため、読み取りサイズを削減し、RCU消費を抑えることができます。
書き込みコスト(WCU)と設計の関係
書き込みは、1KBあたり1WCUが基本です。ここで効いてくるのが、
- アイテムサイズ
- GSIの有無
- 二重書き込みの有無
GSIを1つ追加すると、
- メインテーブルへの書き込み
- GSIへの書き込み
が必ず両方発生します。つまり、GSIは読み取りを安くする代わりに書き込みを確実に高くする仕組みです。
ストレージコストは非正規化の代償
DynamoDBでは、データ重複や非正規化が前提になります。その結果、
- 同じ情報を複数箇所に持つ
- ストレージサイズが増える
という構造になります。ただしストレージ単価自体は比較的安いため、問題になりやすいのはストレージ量よりも読み書きです。
設計がコストに直結する具体例
良い設計としては、
- GetItem/Queryのみ
- GSIは最小限
- アイテムサイズが小さい
→ アクセス数に比例した、予測しやすいコスト算出が可能
一方、悪い設計としては、
- Scanが多い
- PK設計が曖昧
- GSIを後付けで増やす
- 表示最適化のための過剰な複製
→ トラフィックが増えるほど急激にコスト増
RDBではクエリが遅くなったらインデックスを貼ったり、JOINが増えたらチューニングするといった後からの最適化が可能ですが、DynamoDBでは設計時点でコストの上限がほぼ決まるという点が最大の違いです。また、DynamoDB のコストは読み書きで消費したデータ容量(RCU / WCU)に基づいて決まるため、読み書きの実行回数は影響しないという点は重要です。
また、ホットパーティションを意識することは重要です。DynamoDBはPKごとにデータを分散配置します。そのため、
- 特定のPKにアクセスが集中する
- 書き込み・読み取りが偏る
と、ホットパーティションが発生します。よくある原因としては
- 種類の少ないデータ(わかりやすい例だとBooleanなど)をGSIに設定して、特定ノードにデータが集中する。
- 一部のユーザーや商品だけ極端にアクセスが多い
などです。対策の考え方としては、
- アクセスが集中しないPK設計にする(そもそもPKとして適切か)
- 必要に応じてシャーディング(サフィックス付与)を検討する。例えば、access_flagというbooleanのデータをGSIで指定していたとして、今のままだとtrue,falseの2種類しかないのでホットパーティションが発生する可能性が高い。それを防ぐために、access_flag#questionnaire_idみたいにしてアンケートid毎に分散させるなどして、ホットパーティションの状態を防げる。もし、アンケートを跨いだ集計などがアクセスパターンでない場合、集計時にパーティションを跨ぐことはないので、より効率的に集計が可能となる。
まとめ:DynamoDBは設計力が問われるDB
DynamoDBは、RDBと比べて使いにくいDBに見えることがあります。
- JOIN ができない
- 自由な検索ができない
- 正規化できない
- 設計を間違えると修正が大変
しかしこれらは欠点というより、スケールと安定したレイテンシを最優先した結果、意図的に選ばれた制約です。この記事を通して一貫して伝えてきたのは、次の点です。
- DynamoDBはアクセスパターンから設計するDB
- テーブル設計はデータ構造ではなくクエリのために行う
- 非正規化とデータ複製は前提であり、例外ではない
- コストは設計の結果として現れる
RDBのように、まず正規化して後からクエリを最適化するというアプローチは、DynamoDBではほぼ通用しません。DynamoDB を使うべきか迷ったときは、次の問いを自分に投げると判断しやすくなります。
- アクセスパターンは事前に明確か?
- GetItem / Query で完結できるか?
- アクセスパターンに割り切った設計ができるか?
- 一時的不整合やデータ複製を許容できるか?
- スケールや運用をDBに任せたいか?
これらにYESが多いなら、DynamoDBは非常に強力な選択肢になります。一方で、細かい整合性や複雑な関係性の管理が重要なら、DynamoDBを選ばないという判断も正しいという点も、忘れてはいけません。最後にDynamoDBは、魔法のDBでも、万能なDBでもないが、前提が合えばこれ以上なく頼れるDBです。この記事が、DynamoDBをRDBと同様に有用な選択肢として捉えるきっかけになれば幸いです。
最後まで読んでいただきありがとうございました!