DynamoDB の一対多リレーションシップ
SaaS のコントロールプレーンには、ほぼ必ず包含階層があります。1つの ワークスペース
が多くの プロジェクト を所有する、というものです。SQL なら projects テーブルに
workspace_id 外部キーを置いて JOIN するでしょう。
DynamoDB には結合も外部キーもないため、リレーションシップは キースキーマ そのものに
住まわせる必要があります。正しく設計すれば、「ワークスペースとその中のすべての
プロジェクトを読み込む」が、1回の読み取りに後追いのスキャンが続く形ではなく、単一の
Query になります。
DynamoDB で一対多リレーションシップをモデリングするには?
親とすべての子に同じパーティションキーを与えて同じアイテムコレクションに収め、ソートキーで区別します。DynamoDB には結合も外部キーもないため、リレーションシップはキースキーマ自体に住まわせます。こうすることで、親と全ての子の読み込みが結合の代わりに単一の Query になります。
- エンティティではなく読み取りをモデリングする。 一対多リレーションシップは 「ワークスペースのプロジェクトを一覧する」に応えるためだけに存在します — そのクエリを 中心にキーを形作りましょう。
- 親を子のパーティションキーにエンコードする。 ワークスペースとそのすべての プロジェクトに同じパーティションキー値を与え、1つの アイテムコレクション に まとまるようにします。
- そうすれば一覧の読み取りは1回の
Query。 親と任意の数の子が、結合も2回目の ラウンドトリップもなしに、単一の課金対象呼び出しで返ってきます。 - ホットパーティションに注意。 巨大なテナント1つがすべてのトラフィックを1つの パーティションに集中させます。巨大なワークスペースには、シャーディングしたキーと ファンアウトの読み取りが必要になることがあります。
まずアクセスパターンから
DynamoDB のモデリングはエンティティ優先ではなくアクセスパターン優先です — シングルテーブル設計 を支えるのと同じ規律です。どんな キーを選ぶ前にも、アプリが実際に発行する読み取りを書き出しましょう。
- あるワークスペースの設定を取得する。
- あるワークスペースのすべてのプロジェクトを新しい順に一覧する。
- 特定のプロジェクトを id で取得する。
「1つのワークスペース、多くのプロジェクト」というリレーションシップが意味を持つのは、 読み取り #2 があるからこそです。ワークスペースのプロジェクトをまとめて一覧する必要が 一度もないなら、リレーションシップをモデリングすること自体しません — プロジェクトを 独立して保存するでしょう。
つまり問いは抽象的な「一対多をどう表現するか」では決してありません。「このリレーション シップはどのクエリに応えなければならないか」です。それに答えてから、キーをそのまわりに 形作りましょう。
なぜ外部キーはここで役に立たないのか
DynamoDB ではすべての GetItem と Query が パーティションキー を対象とし、
サービスはそのキーをハッシュ化してアイテムを保持するパーティションを特定します。
AWS は Core Components のドキュメントで直接そう述べています。パーティションキー値は、データがどこに住むかを 決める内部ハッシュ関数への入力だ、と。
そのハッシュベースの配置は、キーをノード全体に一貫性ハッシュで分散する 2007 年の原典 Dynamo: Amazon's Highly Available Key-value Store 論文から受け継いだものです。
プロジェクトアイテムに workspace_id という 属性 を素のまま持たせても、その仕組みには
見えません — DynamoDB はそれを「たどる」ことができません。
1回のリクエストで関連アイテムを取得するには、親のアイデンティティをプロジェクトの
パーティションキー にエンコードし、あるワークスペースのすべてのアイテムが同じ
パーティションにハッシュされ、1つの Query でまとめてさらえるようにする必要があります。
具体例:ワークスペースとプロジェクト
汎用的でオーバーロードしたキースキーマを使います。パーティションキーを EntityRef、
ソートキーを Detail と呼びましょう。ワークスペースのアイデンティティは、ワークスペース
アイテムと配下のすべてのプロジェクトの 両方 について EntityRef に入れます。
| EntityRef | Detail | attributes |
|---|---|---|
| WS#acme | META | displayName, region, seatLimit |
| WS#acme | PROJ#2026-0007 | title, status, createdBy |
| WS#acme | PROJ#2026-0042 | title, status, createdBy |
| WS#acme | PROJ#2026-0118 | title, status, createdBy |
| WS#globex | META | displayName, region, seatLimit |
| WS#globex | PROJ#2026-0009 | title, status, createdBy |
ワークスペースとそのすべてのプロジェクトは EntityRef = "WS#acme" を共有するため、
1つのパーティション上で一緒に存在する単一の アイテムコレクション を形成します。
Detail ソートキーがそれらを区別します。META がワークスペースのレコードで、各
プロジェクトは PROJ# プレフィックスとゼロ埋めされた時系列順の id を持つため、
プロジェクトは自然に整列します。
視覚的には、親とその子は1つのパーティション内でソートキー順に積み重なります。
EntityRef = "WS#acme" に対する1回の Query が、その積み重ね全体 — 親と
すべての子 — を単一の読み取りでさらいます。
これで3つのアクセスパターンはそれぞれ1回の呼び出しに収まります。
- ワークスペース設定 —
GetItem(EntityRef="WS#acme", Detail="META")。 - プロジェクトを新しい順に一覧 —
Detail begins_with "PROJ#"を付けたQuery(EntityRef="WS#acme")を、降順(ScanIndexForward = false)で実行。 - 1つのプロジェクト —
GetItem(EntityRef="WS#acme", Detail="PROJ#2026-0042")。
2つ目こそが肝心です。親と任意の数の子が、結合も2回目のラウンドトリップもなしに、
1回 の課金対象 Query で返ってきます。外部キー属性と Scan ではできない動きです。
その begins_with 条件を手で書くのは厄介です — キー条件式とプロジェクション式の
構文に噛まれます。
DynamoDB Expression Builder は、文法と戦わなくて
済むよう、KeyConditionExpression、#name/:value のプレースホルダーマップ、そして
すぐに実行できる SDK スニペットを生成します。
KeyConditionExpression "#er = :er AND begins_with(#d, :p)"
ExpressionAttributeNames { "#er": "EntityRef", "#d": "Detail" }
ExpressionAttributeValues { ":er": "WS#acme", ":p": "PROJ#" }
DynoTable でアイテムコレクションを見る
このレイアウトの見返りは視覚的です。同じ EntityRef を共有するすべての行が、
ワークスペースとその子であり、互いに隣り合って並びます。
DynoTable はそれらをグループ化するので、別々のテーブルにまたがって推測するのではなく、 一対多リレーションシップを1つの連続したブロックとして見られます。

落とし穴と別の形
いくつか注意すべき点があります。
- ホットパーティション。 1つのワークスペースのすべてのアイテムが1つの
パーティションに住むため、非常に大きい、あるいは非常に忙しいテナント1つが
トラフィックを集中させます。AWS が説明する
アダプティブキャパシティ
の挙動は中程度の偏りを吸収しますが、数百万のプロジェクトを持つワークスペースには
シャーディングしたキー(例:
WS#acme#01 … #10)とファンアウトの読み取りが必要に なることがあります。 - アイテムコレクションのサイズ。 ローカルセカンダリインデックスがある場合、単一 パーティションのアイテムコレクションは 10 GB に制限されます。LSI がなければその 制限はありません。ここでインデックスの種類を比較しているなら、 GSI と LSI を参照してください。
ScanではなくQueryに手を伸ばす。 この設計全体は、1つのパーティションをQueryできるようにするために存在します。「ワークスペースのプロジェクトを探す」 ためにフィルタ付きのScanに逃げると、モデルを捨ててテーブル全体を読むことに なります — Query と Scan で扱う罠です。
ワークスペースを またいで プロジェクトを一覧する必要が本当にある場合(例えば
グローバルにすべての status = ACTIVE のプロジェクト)、ベーステーブルはそれに
答えられません — そのパーティションキーはワークスペース単位だからです。
それは、このリレーションシップを作り直す仕事ではなく、プロジェクトを別の属性で 再パーティショニングするセカンダリインデックスの仕事です。
次のステップ
アクセスパターンをモデリングし、親を子のパーティションキーにエンコードすれば、
一対多の読み取りは単一の Query になります。
DynamoDB Expression Builder でキー条件を組み立てて
検証しましょう。
そして DynoTable をダウンロード して、このスキーマを読み込み、 ワークスペース→プロジェクトのアイテムコレクションをライブで閲覧し、各クエリが きっかり1回の読み取りで済むことを確認してください。


