One-to-Many-Relationships in DynamoDB
Eine SaaS-Control-Plane hat fast immer eine Containment-Hierarchie: ein
Workspace besitzt viele Projects. In SQL würdest du einen workspace_id-
Foreign Key auf die Projects-Tabelle legen und JOINen.
DynamoDB hat keine Joins und keine Foreign Keys, also muss die Relationship im
Key Schema selbst leben. Richtig gemacht wird „lade einen Workspace und jedes
Project darin“ zu einer einzelnen Query statt einem Read plus Follow-up-Scan.
Wie modellierst du eine One-to-Many-Relationship in DynamoDB?
Gib dem Parent und allen seinen Children denselben ,
sodass sie eine teilen, und differenziere sie
mit dem Sort Key. DynamoDB hat keine Joins oder Foreign Keys, also lebt die
Relationship im Key Schema selbst. Einen Parent plus jedes Child zu laden wird dann
eine einzelne Query statt eines Joins.
- Modelliere die Reads, nicht die Entities. Die One-to-Many-Relationship existiert nur, um „liste die Projects eines Workspace“ zu bedienen — forme die Keys um diese Query.
- Kodiere den Parent in den des Child. Gib dem Workspace und all seinen Projects denselben Partition-Key-Wert, sodass sie in einer landen.
- Dann ist der List-Read eine
Query. Parent plus seine Children kommen zusammen zurück — kein Join, kein zweiter Round Trip (eineQuerygibt bis zu 1 MB pro Page zurück und paginiert danach viaLastEvaluatedKey). - Achte auf die . Ein riesiger Tenant konzentriert all seinen Traffic auf eine Partition; ein gigantischer Workspace braucht vielleicht einen geshardeten Key und einen Fan-out-Read.
Zuerst das Access Pattern
DynamoDB-Modeling ist Access-Pattern-first, nicht Entity-first — dieselbe Disziplin hinter Single-Table Design. Bevor du irgendeinen Key wählst, schreib die Reads auf, die die App wirklich ausstellt:
- Die Settings eines Workspace holen.
- Jedes Project in einem Workspace listen, neueste zuerst.
- Ein spezifisches Project per Id holen.
Die „ein Workspace, viele Projects“-Relationship zählt nur wegen Read #2. Wenn du nie die Projects eines Workspace zusammen listen müsstest, würdest du die Relationship gar nicht modellieren — du speicherst Projects unabhängig.
Die Frage ist also nie abstrakt „wie repräsentiere ich One-to-Many?“. Sie ist „welche Queries muss diese Relationship bedienen?“ Beantworte das, dann forme die Keys darum.
Warum ein Foreign Key hier nicht hilft
In DynamoDB targetet jedes GetItem und jede Query einen Partition Key, und
der Service hasht diesen Key, um die Partition zu lokalisieren, die das Item hält.
AWS sagt das direkt in den Core Components-Docs: der Partition-Key-Wert ist Input einer internen Hash-Funktion, die entscheidet, wo Daten leben.
Diese Hash-basierte Platzierung ist die Erbschaft aus dem originalen 2007-Dynamo: Amazon's Highly Available Key-value Store-Paper, wo Consistent Hashing Keys über Nodes verteilt.
Ein nacktes workspace_id-Attribut auf einem Project-Item ist für diese
Maschinerie unsichtbar — DynamoDB kann ihm nicht „folgen“.
Um related Items in einem Request zu fetchen, muss die Identität des Parent in den
Partition Key des Project kodiert sein, sodass alle Items eines Workspace zur
selben Partition hashen und eine Query sie fegen kann.
Praxisbeispiel: Workspaces und Projects
Nutze ein generisches, überladenes Key Schema. Nenne den Partition Key EntityRef
und den Sort Key Detail. Die Workspace-Identität geht für beide — das
Workspace-Item und jedes Project darunter — in 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 |
Der Workspace und all seine Projects teilen EntityRef = "WS#acme", also bilden
sie eine einzelne Item Collection, die zusammen auf einer Partition lebt.
Der Detail-Sort-Key trennt sie: META ist der Workspace-Record, und jedes
Project trägt ein PROJ#-Prefix mit einer zero-padded, zeitgeordneten Id, sodass
Projects natürlich sortieren.
Visuell stapeln Parent und Children in einer Partition, geordnet nach Sort Key:
Eine Query auf EntityRef = "WS#acme" fegt den ganzen Stack — Parent plus jedes
Child — in einem einzelnen Read.
Jetzt kollabiert jedes der drei Access Patterns zu einem Call:
- Workspace-Settings —
GetItem(EntityRef="WS#acme", Detail="META"). - Projects neueste zuerst listen —
Query(EntityRef="WS#acme")mitDetail begins_with "PROJ#", in absteigender Ordnung (ScanIndexForward = false). - Ein Project —
GetItem(EntityRef="WS#acme", Detail="PROJ#2026-0042").
Das zweite ist der Punkt. Parent und Children kommen aus einer Query zurück,
kein Join und kein zweiter Round Trip — DynamoDB gibt bis zu 1 MB pro Page zurück
und reicht dir einen LastEvaluatedKey für den Rest. Das ist der Move, den du mit
einem Foreign-Key-Attribut und einem Scan nicht machen kannst.
Diese begins_with-Condition von Hand zu schreiben ist fiddly — Key-Condition- und
Projection-Expression-Syntax beißen.
Der DynamoDB Expression Builder generiert die
KeyConditionExpression, die #name/:value-Placeholder-Maps und ein ready-to-
run SDK-Snippet, sodass du nicht mit der Grammar kämpfst:
KeyConditionExpression "#er = :er AND begins_with(#d, :p)"
ExpressionAttributeNames { "#er": "EntityRef", "#d": "Detail" }
ExpressionAttributeValues { ":er": "WS#acme", ":p": "PROJ#" }
Die Item Collection in DynoTable inspizieren
Jede Row, die einen EntityRef teilt, ist der Workspace plus seine Children,
nebeneinander.
DynoTable gruppiert sie, sodass du die One-to-Many-Relationship als einen zusammenhängenden Block siehst, statt sie über separate Tabellen zu erraten.

Fallstricke und die alternative Form
Ein paar Dinge zum Beobachten:
- Hot Partitions. Jedes Item für einen Workspace lebt auf einer Partition, also
konzentriert ein einzelner sehr großer oder sehr busy Tenant Traffic. Das
Adaptive Capacity-Verhalten,
das AWS beschreibt, absorbiert moderate Skew, aber ein Workspace mit Millionen
Projects braucht vielleicht einen geshardeten Key (z. B.
WS#acme#01 … #10) und einen Fan-out-Read. - Item-Collection-Größe. Mit einem Local Secondary Index ist die Item Collection einer einzelnen Partition bei 10 GB capped; ohne LSI gibt es kein solches Limit. Wenn du hier Index-Typen abwägst, siehe GSI vs LSI.
- Greif zu
Query, nieScan. Das ganze Design existiert, damit du eine PartitionQueryen kannst. Auf einen gefiltertenScanzurückzufallen, um „Projects eines Workspace zu finden“, wirft das Modell weg und liest die ganze Tabelle — die Falle in Query vs Scan.
Wenn du Projects wirklich über Workspaces listen musst (z. B. alle
status = ACTIVE Projects global), kann die Base Table das nicht beantworten —
ihr Partition Key ist workspace-scoped.
Das ist ein Job für einen Secondary Index, der Projects auf einem anderen Attribut re-partitioniert, nicht dafür, diese Relationship umzuformen.
Nächste Schritte
Modelliere die Access Patterns, kodiere den Parent in den Partition Key des Child,
und der One-to-Many-Read ist eine einzelne Query. Baue und validiere die Key
Condition mit dem
DynamoDB Expression Builder — und wenn du
lieber von den Access Patterns selbst starten willst, draftet das freie
Single-Table-Design-Tool den PK/SK/GSI-Plan
mit Example Items.
Dann lade DynoTable herunter, um dieses Schema zu laden, die
Workspace→Projects-Item-Collection live zu browsen und zu bestätigen, dass jede
Query genau einen Read macht. Wenn du Workspaces und Projects lieber als joined,
relationale View sehen willst, führt
DynoTables SQL Workbench diesen JOIN auch.


