Many-to-Many-Relationships in DynamoDB
Ein Student schreibt sich in viele Courses ein; ein Course hält viele Students. In
SQL greifst du nach einer Join-Tabelle und einem Three-Table-JOIN.
DynamoDB hat keine Joins, also muss die Relationship in den Keys leben — und
der Trick ist, jede Enrollment-Edge in einer Form zu speichern, die beide Seiten
direkt Queryen können.
Dieser Guide läuft das Students ↔ Courses-Problem end to end: die Access Patterns, das -Pattern, das sie löst, ein originales Key Schema zum Kopieren und wie du beide Richtungen zurückliest, ohne je die Tabelle zu scannen.
Wie modellierst du eine Many-to-Many-Relationship in DynamoDB?
DynamoDB hat keine Joins, also modellierst du Many-to-Many mit dem -Pattern: speichere jeden Link als eigenes Edge- Item, keyed by einer Seite, und füge dann einen invertierten GSI hinzu, der die Keys tauscht. Eine einzelne Edge, einmal geschrieben, beantwortet dann Queries aus beiden Richtungen günstig.
- Speichere jede Enrollment als eigenes Edge-Item, nicht als List-Attribut auf einer Seite.
- Keye die Edge nach dem Student (
PK = STU#…,SK = ENROLL#CRS#…), sodass eineQuerydie ganze Course-Liste eines Students zurückgibt. - Füge einen invertierten hinzu, der die Rollen tauscht
(
GSI1PK = CRS#…), sodass dieselbe Edge auch „wer ist in diesem Course?“ beantwortet. - Eine Edge, einmal geschrieben, liest günstig beide Wege — das ist das ganze Spiel.
Zuerst die Access Patterns framen
DynamoDB-Modeling ist Access-Pattern-first: du entscheidest die Reads, bevor du einen einzigen Attributnamen wählst. Eine Many-to-Many-Relationship hat fast immer zwei symmetrische Reads plus die Entity-Lookups:
- Das Profil eines Students holen und jeden Course listen, in dem der Student enrolled ist.
- Die Metadata eines Course holen und jeden Student listen, der in dem Course enrolled ist.
- Eine einzelne Enrollment-Edge nachschlagen — um eine Grade zu updaten oder den Course zu droppen.
Die zwei List-Reads zeigen in entgegengesetzte Richtungen über dieselbe Menge
Edges. Ein naives Design bedient eine günstig und erzwingt einen Scan für die
andere — genau der Fallstrick in Query vs Scan.
Der Job ist, beide Richtungen zu einer einzelnen Query zu machen.
Das Adjacency-List-Pattern nutzen
DynamoDBs eigene Guidance für Relationships ist die Adjacency List: modelliere jede Relationship als Item, dessen Partition Key ein Endpoint ist und dessen Sort Key der andere.
AWS dokumentiert das auf der Seite Best Practices for Managing Many-to-Many Relationships des DynamoDB Developer Guide.
Warum Keys und keine zweite Tabelle? Weil das Primitive, das DynamoDB dir gibt,
eine Query gegen eine einzelne Partition ist.
Eine Query liest eine contiguous Range von Sort-Key-Werten unter einem Partition
Key in einer abgerechneten Operation — das ist der einzige „Join“, den die Engine
bietet.
Um eine Relationship zu bekommen, die von beiden Seiten günstig liest, duplizierst du die Edge: schreibe sie einmal keyed by dem Student, dann nutze einen Secondary Index, um dieselbe Edge keyed by dem Course zu projeizieren.
Das ist das Overloaded-Key-Thinking aus Single-Table Design, angewandt auf eine Relationship statt einer Parent-Child-Hierarchie.
Die Form sind zwei gestapelte Views derselben Edge — die Base Table keyed by Student, der invertierte GSI keyed by Course:
Jede Edge wird einmal auf der Base Table geschrieben und mit getauschten Keys in
den GSI projeziert, sodass eine Query gegen entweder Partition die Relationship
günstig liest.
Die Lineage geht zurück zum 2007 Amazon Dynamo Paper: der Partition Key ist die Einheit der Distribution, und Single-Key-Access ist der Fast Path.
Relationships in DynamoDB sind eine Übung darin, Many-to-Many-Reads in diesen Fast Path zu biegen.
Das Beispiel durcharbeiten: Students ↔ Courses
Nutze eine Tabelle mit generischen Keys, PK und SK, und kodiere den Entity-Typ
im Wert. Die Enrollment-Edge ist das Herz davon:
| 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 |
Eine einzelne Query PK = "STU#a91" gibt das Profil des Students und jede
Enrollment in einem Read zurück. Verenge sie mit SK begins_with "ENROLL#", um
nur die Course-Edges zu bekommen. Das löst „liste die Courses eines Students“.
Aber „liste die Students eines Course“ zeigt die andere Richtung — und die Base Table kann das nicht beantworten, weil die Student-Id im Partition Key steckt, nicht im Sort Key.
Füge einen invertierten Global Secondary Index hinzu, der die Rollen tauscht.
Gib den Edge-Items ein generisches GSI1PK/GSI1SK-Paar, das den Course auf der
Partition-Seite und den Student auf der Sort-Seite hält:
| 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 |
Jetzt listet Query GSI1 WHERE GSI1PK = "CRS#math204" jeden Student in diesem
Course — den Read, den die Base Table nicht bedienen konnte. Ein Edge-Item, einmal
geschrieben, beantwortet beide Richtungen.
Es muss ein GSI sein, kein LSI: die Course-Partition ist völlig anders als die Student-Partition, und ein LSI teilt den Partition Key der Base Table.
Der Index spannt mehrere Partitionen, also muss er global sein — siehe GSI vs LSI.
GSIs in DynamoDB werden asynchron befüllt. Eine brandneue Enrollment kann einen
Moment brauchen, um in der CRS#…-Richtung zu erscheinen.
Behandle den Course-Roster-Read als — was der Developer Guide für Global Secondary Indexes explizit nennt.
In DynoTable schreiben und lesen
Die Enrollment zu schreiben heißt, vier Key-Attribute plus die eigenen Daten der
Edge zu setzen. Die Condition, die stoppt, dass ein Student sich zweimal in denselben
Course einschreibt, ist ein attribute_not_exists(PK)-Guard auf dem Composite Key.
Genau diese Art Condition kannst du visuell mit dem
DynamoDB Expression Builder zusammenbauen,
statt ExpressionAttributeNames und Placeholder-Values von Hand zu schreiben.
In DynoTable zeigst du eine Query auf GSI1, setzt GSI1PK = "CRS#math204", und
der Roster kommt als Tabelle zurück, die du lesen, sortieren und in Place editieren
kannst — beide Richtungen der Relationship browsable aus einem Schema.

Fallstricke und nächste Schritte
- Speichere eine Seite nicht als List-Attribut. Ein
courseIds-Array auf dem Student-Item fühlt sich tidy an, bis ein Course seinen Roster braucht, das Array die 400-KB-Item-Decke trifft oder zwei Enrollments racen und einander clobbern. Diskrete Edge-Items skalieren und updaten unabhängig. - Halte Edge-Daten auf der Edge.
gradeundenrolledOnder Enrollment gehören auf das Edge-Item, nicht dupliziert auf Student oder Course — es gibt genau eine Row pro (Student, Course)-Paar zum Updaten. - Achte auf GSI-Propagation. Die Invertierte-Index-Richtung ist eventually consistent, also kann ein Read unmittelbar nach einer Enrollment um einen Bruchteil einer Sekunde nachhinken.
- Projeziere nur, was der Roster braucht. Eine
KEYS_ONLY- oder enge Projektion hält den GSI klein, wenn die Roster-View nur Ids braucht.
Um tiefer in die umgebenden Patterns zu gehen, lies Single-Table Design für overloaded Keys und GSI vs LSI dafür, wann der invertierte Index global sein muss. Und um von deinen eigenen Relationships zu starten, verwandelt das freie Single-Table-Design-Tool eine Access-Pattern- Liste wie „liste die Courses eines Students / liste die Students eines Course“ in einen PK/SK/GSI-Plan mit Example Items.
Dann lade DynoTable herunter, um das Students ↔ Courses-Schema für real
zu modellieren — schreibe die Edges, baue die Condition mit dem Expression Builder
und query beide Richtungen der Relationship ohne einen einzigen Scan. Und wenn du
die klassische Three-Table-JOIN-View trotzdem willst, führt
DynoTables SQL Workbench sie über deine Live-Tabellen aus.


