DynamoDB の多対多リレーションシップ
1 人の学生は多くの講座に登録し、1 つの講座は多くの学生を抱えます。SQL では結合テーブルと 3 テーブルの JOIN に手を伸ばします。
DynamoDB には結合がないので、リレーションシップは キー の中に存在しなければなりません — そしてコツは、各登録エッジを、両側が直接 Query できる形で保存することです。
このガイドは学生 ↔ 講座の問題を端から端までたどります。アクセスパターン、それらを解決するパターン、コピーできるオリジナルのキースキーマ、そしてテーブルをスキャンすることなく両方向を読み戻す方法です。
DynamoDB で多対多リレーションシップをどうモデル化しますか?
DynamoDB には結合がないので、多対多リレーションシップはパターンでモデル化します。各リンクを、片側でキー付けした独自のエッジ項目として保存し、キーを入れ替えた反転 GSI を追加します。一度書かれた単一のエッジが、両方向からのクエリに安価に答えます。
- 各登録をそれ自身のエッジ項目として保存する — どちらかの側のリスト属性ではなく。
- エッジを学生でキー付けする(
PK = STU#…、SK = ENROLL#CRS#…)ので、1 つのQueryが学生の講座リスト全体を返します。 - 役割を入れ替える反転 を追加する(
GSI1PK = CRS#…)ので、同じエッジが「この講座には誰がいるか?」にも答えます。 - 1 つのエッジ、一度書けば、両方向に安価に読める — それがゲームのすべてです。
まずアクセスパターンを枠組む
DynamoDB のモデリングはアクセスパターンファーストです。単一の属性名を選ぶ前に読み取りを決めます。多対多リレーションシップはほぼ常に 2 つ の対称な読み取りに、エンティティのルックアップを加えたものを持ちます。
- 学生のプロフィールを取得し、その学生が登録しているすべての講座をリストする。
- 講座のメタデータを取得し、その講座に登録しているすべての学生をリストする。
- 単一の登録エッジを引く — 成績を更新するか講座を取り消すために。
痛いところ: 2 つのリスト読み取りは、同じエッジの集合を挟んで正反対の方向を指します。素朴な設計は一方を安価に提供し、もう一方には Scan を強います — Query vs Scan で扱ったまさにその落とし穴です。
仕事は 両方 の方向を単一の Query にすることです。
隣接リストパターンを使う
リレーションシップに対する DynamoDB 自身のガイダンスは 隣接リスト です。各リレーションシップを、パーティションキーが一方の端点、ソートキーがもう一方であるような項目としてモデル化します。
AWS はこれを DynamoDB デベロッパーガイドの多対多リレーションシップの管理のためのベストプラクティスのページで文書化しています。
なぜ 2 つ目のテーブルではなくキーなのか? DynamoDB が与えるプリミティブが、単一のパーティションに対する Query だからです。
Query は 1 つのパーティションキーの下で連続したソートキー値の範囲を 1 回の課金対象操作で読みます — それがエンジンの提供する唯一の「結合」です。
両側 から安価に読めるリレーションシップを得るには、エッジを複製します。学生でキー付けして一度書き、それからセカンダリインデックスを使って同じエッジを講座でキー付けして射影します。
これはシングルテーブル設計のオーバーロードされたキーの考え方を、親子階層ではなくリレーションシップに適用したものです。
形は、同じエッジの 2 つの積み重なったビューです — 学生でキー付けしたベーステーブルと、講座でキー付けした反転 GSI です。
各エッジはベーステーブルに一度書かれ、キーを入れ替えて GSI に射影されるので、どちらのパーティションに対する Query もリレーションシップを安価に読みます。
その系譜は 2007 年の Amazon Dynamo 論文にさかのぼります。パーティションキーが分散の単位であり、単一キーアクセスが高速経路です。
DynamoDB のリレーションシップは、多対多の読み取りをその高速経路に曲げ込む練習です。
例を作る: 学生 ↔ 講座
汎用のキー PK と SK を持つ 1 つのテーブルを使い、エンティティ型を値の中にエンコードします。登録エッジがその核心です。
| PK | SK | attributes |
|---|---|---|
| STU#a91 | PROFILE | name, year, major |
| STU#a91 | ENROLL#CRS#math204 enrolledOn, grade | |
| STU#a91 | ENROLL#CRS#cs101 | enrolledOn, grade |
| CRS#math204 | METADATA | title, credits, term |
| CRS#cs101 | METADATA | title, credits, term |
単一の Query PK = "STU#a91" は、学生のプロフィール と すべての登録を 1 回の読み取りで返します。SK begins_with "ENROLL#" で絞れば、講座エッジだけを取得できます。それで「学生の講座をリストする」が解決します。
しかし「講座の学生をリストする」は逆方向を指します — そしてベーステーブルはそれに答えられません。学生 id がソートキーではなくパーティションキーにあるからです。
役割を入れ替える反転 グローバルセカンダリインデックス を追加します。エッジ項目に汎用の GSI1PK/GSI1SK の対を与え、講座をパーティション側、学生をソート側に保ちます。
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| STU#a91 | ENROLL#CRS#math204 | CRS#math204 | STU#a91 |
| STU#b30 | ENROLL#CRS#math204 | CRS#math204 | STU#b30 |
| STU#a91 | ENROLL#CRS#cs101 | CRS#cs101 | STU#a91 |
いまや Query GSI1 WHERE GSI1PK = "CRS#math204" は、その講座のすべての学生をリストします — ベーステーブルが提供できなかった読み取りです。1 つのエッジ項目、一度書けば、両方向に答えます。
これは LSI ではなく GSI でなければなりません。講座のパーティションは学生のパーティションとまったく異なり、LSI はベーステーブルのパーティションキーを共有するからです。
インデックスは複数のパーティションにまたがるので、グローバルでなければなりません — GSI vs LSI を参照してください。
1 つ落とし穴: DynamoDB の GSI は非同期に投入されます。真新しい登録が CRS#… 方向に現れるまで少し時間がかかることがあります。
講座名簿の読み取りはとして扱ってください — デベロッパーガイドがグローバルセカンダリインデックスについて明示的に指摘していることです。
DynoTable で書いて読む
登録を書くとは、4 つのキー属性に加えてエッジ自身のデータをセットすることです。学生が同じ講座に二重に登録するのを止める条件は、複合キーへの attribute_not_exists(PK) ガードです。
それはまさに、ExpressionAttributeNames とプレースホルダー値を手で書く代わりに DynamoDB 式ビルダーで視覚的に組み立てられる種類の条件です。
DynoTable では Query を GSI1 に向け、GSI1PK = "CRS#math204" をセットすると、名簿がテーブルとして返り、その場で読み、ソートし、編集できます — リレーションシップの両方向が 1 つのスキーマから閲覧可能です。

落とし穴と次のステップ
- 片側をリスト属性として保存しない。 学生項目上の
courseIds配列は、講座が名簿を必要とするまで、配列が 400 KB の項目上限に達するまで、あるいは 2 つの登録が競合して互いを潰すまで、整って見えます。個別のエッジ項目は独立してスケールし更新されます。 - エッジのデータはエッジに保つ。 登録の
gradeとenrolledOnはエッジ項目に属し、学生や講座に複製されるものではありません — (学生, 講座) の対ごとに更新する行はちょうど 1 つです。 - GSI の伝播に注意する。 反転インデックスの方向は結果整合性なので、登録直後の読み取りは一瞬遅れることがあります。
- 名簿が必要とするものだけを射影する。 名簿ビューが id だけを必要とするなら、
KEYS_ONLYや狭い射影が GSI を小さく保ちます。
周辺のパターンを深く掘るには、オーバーロードされたキーについてシングルテーブル設計を、反転インデックスがグローバルでなければならないのはいつかについて GSI vs LSI を読んでください。そして、自分のリレーションシップから始めるなら、無料のシングルテーブル設計ツールが、「学生の講座を一覧する / 講座の学生を一覧する」のようなアクセスパターンのリストを、例のアイテム付きの PK/SK/GSI プランに変換してくれます。
それから DynoTable をダウンロードして、学生 ↔ 講座のスキーマを実際にモデル化してみてください — エッジを書き、Expression Builder で条件を構築し、リレーションシップの両方向を 1 度のスキャンもなしに照会します。そして、古典的な 3 テーブルの JOIN ビューがやはり欲しくなったら、DynoTable の SQL Workbench がライブのテーブルに対してそれを実行します。


