DynamoDB シングルテーブル設計 完全ガイド
SQL から来ると、エンティティごとに1テーブル — customers、orders、order_items — という直感が働きます。DynamoDB では、その直感はたいてい間違いです。すべてのエンティティを格納し、オーバーロードしたキーのプレフィックスで区別する1つのテーブルなら、親とその子を1回の Query で取得できます。結合も N+1 もありません。
DynamoDB のシングルテーブル設計とは?
シングルテーブル設計は、すべてのエンティティ — 顧客、注文、注文明細 — を1つの DynamoDB テーブルに格納し、オーバーロードしたパーティションキーとソートキーのプレフィックスで区別します。キーがエンティティではなくアクセスパターンを中心に設計されているため、親とそのすべての子は1つのアイテムコレクションに収まり、単一の Query で返ってきます。結合も N+1 の読み取りもありません。
発想
汎用的なキー名(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回読むだけで、顧客とすべての注文がまとめて返ってきます。
名詞ではなく、アクセスパターンから始める
シングルテーブル設計は、エンティティの箱を並べた ER 図ではなく、アプリケーションが実際に行う
読み書きに番号を振ったリストから始まります — 「顧客プロフィールを表示する」「顧客のオープンな
注文を一覧する」「注文を id でグローバルに引く」といった具合です。それぞれのパターンは、
ベーステーブルかオーバーロードした GSI に対する Query または GetItem にコンパイルできなければ
なりません。あるパターンに Scan が必要なら、キーが間違っています。出荷する前にモデルを直しましょう。
リストは明示的に書き出してください。3 パターンのアプリなら、たとえばこうなります。
- ある顧客 id の顧客情報とすべての注文を取得する(高頻度)。
- 顧客横断でオープンな注文をすべて日付順に一覧する(中頻度)。
- 公開用の注文 id で注文を 1 件取得する(高頻度)。
パターン 1 は PK = CUSTOMER#<id> に対するベーステーブルの Query です。パターン 2 は
GSI1PK = STATUS#OPEN、GSI1SK を日付として着地します。パターン 3 は、パターン 1 の
CUSTOMER#<id> / ORDER#<id> の行とは別のアイテムコレクションとして
PK = ORDER#<id>, SK = METADATA を使うかもしれません — 両方のアクセス経路がホットなら注文の
サマリーを二重に持ち、片方がまれなら 2 回目の Query を受け入れます。
無料のシングルテーブル設計ツールは、そのパターンのリストを 受け取り、PK/SK/GSI のレイアウトを、サンプルアイテム、CreateTable の JSON、パターンごとのコストの 目安とともに出力します。コミットする前に、いくつかの案を比べられます。
オーバーロードされた 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 回の Query と多数の GetItem
子アイテムが親のパーティションキーの下にあるとき、課金面の利点は本物です。顧客プロフィール
(1 KB)と 12 件の注文サマリー(各 1 KB)が PK = CUSTOMER#42 を共有しているとしましょう。
そのパーティションに対する結果整合性の Query 1 回は合計 13 KB を読み、2 読み取り
リクエストユニットです(13 KB は 4 KB ブロック 4 つに切り上がり、各 0.5 RCU)。同じ 13 件を
13 回の GetItem で取ると、同じ切り上げがアイテムごとに個別に効いて 7 ユニットになります
(13 × 0.5 RCU、アイテムごとに最低 1 ブロック)。同じデータで読み取りユニットは 3 倍以上 —
しかもネットワークの往復は数えていません。
| アクセスの仕方 | 往復回数 | 読み取りユニット(1 KB × 13 件、結果整合性) |
|---|---|---|
Query PK = CUSTOMER#42 | 1 | 2 |
完全なプライマリキーで 13 × GetItem | 13 | 7 |
Scan + 顧客 id でのフィルター | 1 ページ以上 | テーブル全体 |
数字はアイテムサイズで変わります — 6 KB のプロフィールのブロブなら、1 件で 4 KB ブロックを 2 つ またぎます — だからこそアイテムサイズ計算ツールが設計 ツールの隣に置いてあります。書き込みは 1 KB の切り上げ境界に従い(1 キロバイトあたり 1 WCU、 トランザクション内では倍)、太った注文の行は親のパーティションに非正規化するコストが高くつきます。
エンティティの型付けとスパースインデックス
多くのチームは、Query がソートキーの接頭辞が混ざった結果を返したときにアプリケーション
コードで分岐できるよう、文字列の type や entity 属性(CUSTOMER、ORDER、LINE)を
足します。GSI はスパースにできます。GSI1PK/GSI1SK を埋めたアイテムだけがインデックスに
現れるので、それらの属性が空の顧客プロフィールの行は、インデックスのストレージを消費せず、
無関係なクエリにも出てきません。
2 つのエンティティ型が 1 つのインデックスを共有するときは、それぞれの接頭辞(STATUS#、
CATEGORY#、EMAIL#)が何を意味するのかを、ベーステーブルの接頭辞を記録しているのと同じ
場所に書いておきましょう。将来の自分が、CloudWatch のグラフだけから意味を逆算する羽目に
なってはいけません。
シングルテーブルにしないとき
無料ではありません。1つのオーバーロードしたテーブルは、推論しづらく、進化させづらく、分析に不向きです。アクセスパターンが本当に不明だったり絶えず変わったり、データがほぼ分析用途だったりするなら、テーブルを分ける(あるいは別のストアを使う)ほうが正気の判断になり得ます。シングルテーブルが勝つのは、パターンが既知で高ボリュームのときです。
| シグナル | テーブルを分ける寄り | シングルテーブル寄り |
|---|---|---|
| アクセスパターンが文書化されていて安定している | ✓ | |
| 共有する読み取りのない無関係なドメインが多い | ✓ | |
| DynamoDB に不慣れなチームで、見て分かるテーブル名が要る | ✓ | |
| 親と子を常にまとめて取得する | ✓ | |
| 任意の結合をまたぐアドホック分析が多い | ✓(またはデータウェアハウス) | |
| 1 つのパーティションキー上の高頻度なマルチエンティティのダッシュボード | ✓ |
規制上あるいは組織上の境界(データの所有者が異なる、バックアップの方針が別)も、シングルテーブルのモデルがアクセスパターンに技術的には合っている場合でさえ、テーブルを複数に分ける理由になり得ます。
誤った形のコスト
テーブルを分けてモデル化すると、顧客を組み立て直すために Scan かクライアント側の結合を強いられ、それが Scan の落とし穴です。まずアクセスパターンをモデル化し、それからキーを設計して各パターンを Query にしてください。(モデル化していなかったアドホックなエンティティ横断の質問には、DynoTable の SQL Workbenchが JOIN をクライアント側で実行します — 探索のために再モデル化を待つ必要はありません。)
稼働中のシングルテーブルのレイアウトを進化させるというのは、たいてい PK/SK の意味をその場で
付け替えることではなく、GSI や新しいアイテム型を足すことです。接頭辞の取り決めは API の
バージョンのように扱いましょう。ORDER# を途中で流用するのではなく、ORDER#v2# を足します。
すべてのアイテムのキーを書き換える移行は、金曜午後のスクリプトではなく、二重書き込み期間を
設けた管理されたバックフィルの仕事です。
設計そのものは無料のシングルテーブル設計ツールで
下書きしましょう — アクセスパターンのリストを、例のアイテムとコストのヒント付きの
PK/SK/GSI プランに変換してくれます。
これらのアイテムが 1 回の読み取りでいくらかかるかは
アイテムサイズ&キャパシティ計算機で見積もり、
SDK のコードを出荷する段になったら、そこから生まれる Query のプログラムを
クエリビルダーで組み立ててください。
パーティションキーとソートキーの仕組みがまだ曖昧なら、
複合プライマリキーを読みましょう — シングルテーブル設計は、
その大部分が、属性名を共有した規律ある複合キーの使い方です。
シングルテーブルのスキーマをブラウズしてオーバーロードされたコレクションを並べて見るには DynoTable を試してください — パーティションインスペクタのビューが、ソートキーの順序を、 あなたが下書きしたアクセスパターンと並べて示します。