DynamoDB のシングルテーブル設計
SQL から来ると、本能的にエンティティごとに1つのテーブル — customers、orders、
order_items — にしたくなります。DynamoDB では、その本能はたいてい誤りです。
オーバーロードしたキーのプレフィックスで区別しながら すべての エンティティを
1つのテーブルに格納すると、親とその子を 1回の Query で取得できます — 結合も
N+1 もありません。
エンティティではなく、アクセスパターンから始める
シングルテーブル設計は アクセスパターン優先 です。単一のキーを選ぶ前に、アプリが
行うすべての読み取りを書き出してください — 「顧客のプロフィールを取得する」「顧客の
注文を新しい順に一覧する」「すべてのオープンな注文を見つける」 — キーはそのリストに
応えるためだけに存在するからです。リレーショナルの正規化はストレージを最適化し、
DynamoDB のモデリングは、すでに実行すると分かっているクエリを最適化します。それらを
列挙し、各クエリが1回の Query になるようにキーを設計してください。
考え方
汎用的なキー名(PK、SK)を選び、エンティティの型を値にエンコードします。
| PK | SK | attributes |
|---|---|---|
| CUSTOMER#42 | PROFILE | name, email, plan |
| CUSTOMER#42 | ORDER#2026-001 | total, status |
| CUSTOMER#42 | ORDER#2026-002 | total, status |
これで1回の Query PK = "CUSTOMER#42" が、プロフィール と すべての注文を、1回の
課金される読み取りで返します。SK begins_with "ORDER#" で注文だけに絞り込めます。
視覚的には、オーバーロードしたアイテムは1つのパーティションキーの下に、単一のアイテムコレクションとして積み重なります。
パーティションを1回読むだけで、顧客とすべての注文がまとめて返ってきます。
オーバーロードした GSI
同じ手法はインデックスでも使えます。アイテムに汎用的な GSI1PK/GSI1SK を付け、
各アイテムがそれらの属性に何を書き込むかに応じて、1つの GSI が複数のアクセス
パターンに応えます。
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| ORDER#001 | METADATA | STATUS#OPEN | 2026-01-04 |
| ORDER#002 | METADATA | STATUS#OPEN | 2026-01-05 |
これで Query GSI1 WHERE GSI1PK = "STATUS#OPEN" がオープンな注文を日付順に一覧します
— ベーステーブルでは答えられないパターンです。別のエンティティは、独自の意味で
GSI1 を再利用できます(例: CATEGORY#books)。1つのインデックスで、多くのクエリ。
多対多: 隣接リスト
リレーションシップ(多くのチームに属するユーザー、多くのユーザーを持つチーム)には、
id を入れ替えてエッジを 2回 書き込みます: PK=USER#1, SK=TEAM#9 と
PK=TEAM#9, SK=USER#1。どちらの側をクエリしても他方を一覧できます — 結合テーブルの
DynamoDB 版です。
シングルテーブルにしないとき
それはタダではありません。1つのオーバーロードしたテーブルは、理解しにくく、進化させ にくく、分析に不向きです。アクセスパターンが本当に未知だったり常に変化したりする、 あるいはデータがほとんど分析用なら、テーブルを分ける(または別のストアを使う)方が 理にかなうこともあります。シングルテーブルが勝つのは、パターンが 既知で大量 の ときです。
形を誤ったときのコスト
別々のテーブルとしてモデル化すると、顧客を再構成するために Scan やクライアント側の
結合を強いられます。それが Scan の落とし穴 です。まず
アクセスパターンをモデル化し、それから各パターンを Query にするキーを設計して
ください。
これらのアイテムが読み取りごとにいくらかかるかを アイテムサイズ & キャパシティ計算ツール で 見積もり、DynoTable を試して シングルテーブルのスキーマを閲覧し、 オーバーロードしたコレクションを並べて確認してください。