DynamoDB の多対多リレーションシップ
1人の学生は多くのコースに登録し、1つのコースは多くの学生を抱えます。SQL なら結合
テーブルと4方向の JOIN に手を伸ばすところです。
DynamoDB には結合がないため、リレーションシップは キー に住まわせる必要があります —
そしてコツは、各登録エッジを、両側が直接 Query できる形で保存することです。
このガイドは学生 ↔ コースの問題を端から端まで歩きます。アクセスパターン、それを解く 隣接リストパターン、コピーして使えるオリジナルのキースキーマ、そしてテーブルを一切 スキャンせずに両方向を読み戻す方法です。
DynamoDB で多対多リレーションシップをモデリングするには?
DynamoDB には結合がないため、多対多リレーションシップは隣接リストパターンでモデリングします。各リンクを一方のキーでキーイングしたエッジアイテムとして保存し、キーを入れ替えた反転 GSI を追加します。1回書いた1つのエッジが、両方向のクエリに安価に答えます。
- 各登録を、どちらかの側のリスト属性ではなく、それ自身のエッジアイテムとして保存する。
- エッジを学生でキーイングする(
PK = STU#…、SK = ENROLL#CRS#…)ので、1回のQueryで学生のコース一覧全体が返ります。 - 役割を入れ替えた反転 GSI を追加する(
GSI1PK = CRS#…)ので、同じエッジが 「このコースには誰がいるか?」にも答えます。 - 1つのエッジを1回書けば、両方向とも安価に読める — それがゲームのすべてです。
まずアクセスパターンを定める
DynamoDB のモデリングはアクセスパターン優先です。1つの属性名を選ぶ前に読み取りを 決めます。多対多リレーションシップには、ほぼ必ず 2つ の対称的な読み取りと エンティティ参照があります。
- 学生のプロフィールを取得し、その学生が登録しているすべてのコースを一覧する。
- コースのメタデータを取得し、そのコースに登録しているすべての学生を一覧する。
- 単一の登録エッジを参照する — 成績を更新したり、コースを取り消したりするため。
つらいのは、2つの一覧読み取りが、同じエッジの集合に対して逆向きを指していることです。
素朴な設計は片方を安価に提供し、もう片方には Scan を強います —
Query と Scan で扱うまさにその落とし穴です。
仕事は、両方向 を1回の Query にすることです。
隣接リストパターンを使う
リレーションシップに対する DynamoDB 自身のガイダンスは 隣接リスト です。各 リレーションシップを、パーティションキーが一方の端点で、ソートキーがもう一方の端点で あるアイテムとしてモデリングします。
AWS は DynamoDB デベロッパーガイドの Best Practices for Managing Many-to-Many Relationships のページでこれを文書化しています。
なぜ2つ目のテーブルではなくキーなのか? DynamoDB が与えてくれるプリミティブが、
単一パーティションに対する Query だからです。
Query は、1つのパーティションキーの下のソートキー値の連続した範囲を、1回の課金対象
操作で読みます — それがこのエンジンが提供する唯一の「結合」です。
両方 の側から安価に読めるリレーションシップを得るには、エッジを複製します。学生で キーイングして1回書き、次にセカンダリインデックスを使って、コースでキーイングした同じ エッジを射影します。
これは シングルテーブル設計 のオーバーロードキーの考え方を、 親子階層ではなくリレーションシップに適用したものです。
形は同じエッジの2つの積み重なったビューです — 学生でキーイングしたベーステーブルと、 コースでキーイングした反転 GSI。
各エッジはベーステーブルに1回書かれ、キーを入れ替えて 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つのエッジアイテムを1回書けば、両方向に
答えます。
それは LSI ではなく GSI でなければなりません。コースのパーティションは学生のパーティション とはまったく異なり、LSI はベーステーブルのパーティションキーを共有するからです。
インデックスは複数のパーティションにまたがるため、グローバルでなければなりません — GSI と LSI を参照してください。
1つ落とし穴があります。DynamoDB の GSI は非同期に populate されます。出来たての登録は、
CRS#… 方向に現れるまでに一瞬かかることがあります。
コースの名簿の読み取りは結果整合性があるものとして扱いましょう — デベロッパーガイドが グローバルセカンダリインデックスについて明示的に指摘していることです。
DynoTable で書いて読む
登録を書くとは、4つのキー属性とエッジ自身のデータを設定することです。同じコースに学生が
二重登録するのを止める条件は、複合キーに対する attribute_not_exists(PK) ガードです。
それはまさに、ExpressionAttributeNames とプレースホルダー値を手書きする代わりに、
DynamoDB Expression Builder で視覚的に組み立てられる
種類の条件です。
DynoTable では Query を GSI1 に向け、GSI1PK = "CRS#math204" を設定すると、名簿が
読んで、ソートして、その場で編集できるテーブルとして返ってきます — リレーションシップの
両方向を1つのスキーマから閲覧できます。

落とし穴と次のステップ
- 片側をリスト属性として保存しない。 学生アイテム上の
courseIds配列は整然として 見えますが、コースが名簿を必要としたとき、配列が 400 KB のアイテム上限に達したとき、 あるいは2つの登録が競合して互いを潰したときに破綻します。個別のエッジアイテムは独立して スケールし更新されます。 - エッジのデータはエッジに置く。 登録の
gradeとenrolledOnはエッジアイテムに 属し、学生やコースに複製しません — (学生, コース) のペアごとに更新する行はちょうど1つ です。 - GSI の伝播に注意。 反転インデックスの方向は結果整合性があるため、登録の直後の 読み取りは1秒の何分の1か遅れることがあります。
- 名簿が必要とするものだけを射影する。 名簿ビューが id だけを必要とするとき、
KEYS_ONLYか狭い射影が GSI を小さく保ちます。
周辺のパターンをさらに深掘りするには、オーバーロードキーについて シングルテーブル設計 を、反転インデックスがグローバルで なければならない理由について GSI と LSI を読んでください。
そして DynoTable をダウンロード して、学生 ↔ コースのスキーマを実際に モデリングしましょう — エッジを書き、Expression Builder で条件を組み立て、スキャン1回 なしにリレーションシップの両方向をクエリしてください。


