中級読了 3 分

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。

反転 GSI1 コースでキーイングベーステーブル 学生でキーイング同じエッジ、入れ替えたキー同じエッジ、入れ替えたキーPK STU#a91SK ENROLL#CRS#math204PK STU#a91SK ENROLL#CRS#cs101GSI1PK CRS#math204GSI1SK STU#a91GSI1PK CRS#cs101GSI1SK STU#a91

各エッジはベーステーブルに1回書かれ、キーを入れ替えて GSI に射影されるため、どちらの パーティションに対する Query もリレーションシップを安価に読みます。

その系譜は 2007 年の Amazon Dynamo 論文 にさかのぼります。パーティションキーは分散の単位であり、単一キーアクセスが高速パスです。

DynamoDB のリレーションシップは、多対多の読み取りをその高速パスへと曲げる練習です。

例を解く:学生 ↔ コース

汎用キー PKSK を持つ1つのテーブルを使い、エンティティの種類を値にエンコード します。登録エッジがその核心です。

PKSKattributes
STU#a91PROFILEname, year, major
STU#a91ENROLL#CRS#math204 enrolledOn, grade
STU#a91ENROLL#CRS#cs101enrolledOn, grade
CRS#math204METADATAtitle, credits, term
CRS#cs101METADATAtitle, credits, term

単一の Query PK = "STU#a91" は、学生のプロフィール すべての登録を1回の読み取りで 返します。SK begins_with "ENROLL#" で絞り込めば、コースのエッジだけが得られます。これで 「学生のコースを一覧する」が解けます。

しかし「コースの学生を一覧する」は逆向きを指しています — そしてベーステーブルはそれに 答えられません。学生 id がソートキーではなくパーティションキーにあるからです。

役割を入れ替えた反転 グローバルセカンダリインデックス を追加します。エッジアイテムに 汎用の GSI1PK/GSI1SK のペアを与え、パーティション側にコース、ソート側に学生を 持たせます。

PKSKGSI1PKGSI1SK
STU#a91ENROLL#CRS#math204CRS#math204STU#a91
STU#b30ENROLL#CRS#math204CRS#math204STU#b30
STU#a91ENROLL#CRS#cs101CRS#cs101STU#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 では QueryGSI1 に向け、GSI1PK = "CRS#math204" を設定すると、名簿が 読んで、ソートして、その場で編集できるテーブルとして返ってきます — リレーションシップの 両方向を1つのスキーマから閲覧できます。

DynoTable で反転 GSI をクエリして、コースに登録したすべての学生を一覧。
DynoTable で反転 GSI をクエリして、コースに登録したすべての学生を一覧。

落とし穴と次のステップ

  • 片側をリスト属性として保存しない。 学生アイテム上の courseIds 配列は整然として 見えますが、コースが名簿を必要としたとき、配列が 400 KB のアイテム上限に達したとき、 あるいは2つの登録が競合して互いを潰したときに破綻します。個別のエッジアイテムは独立して スケールし更新されます。
  • エッジのデータはエッジに置く。 登録の gradeenrolledOn はエッジアイテムに 属し、学生やコースに複製しません — (学生, コース) のペアごとに更新する行はちょうど1つ です。
  • GSI の伝播に注意。 反転インデックスの方向は結果整合性があるため、登録の直後の 読み取りは1秒の何分の1か遅れることがあります。
  • 名簿が必要とするものだけを射影する。 名簿ビューが id だけを必要とするとき、 KEYS_ONLY か狭い射影が GSI を小さく保ちます。

周辺のパターンをさらに深掘りするには、オーバーロードキーについて シングルテーブル設計 を、反転インデックスがグローバルで なければならない理由について GSI と LSI を読んでください。

そして DynoTable をダウンロード して、学生 ↔ コースのスキーマを実際に モデリングしましょう — エッジを書き、Expression Builder で条件を組み立て、スキャン1回 なしにリレーションシップの両方向をクエリしてください。

更新日