Denormalisierung in DynamoDB
Aus SQL kommend klingt Denormalisierung wie eine Sünde — duplizierte Daten, keine einzige Source of Truth. In DynamoDB ist genau das der Punkt. Es gibt keine Joins, also kopierst du verwandte Daten auf das Item, das sie braucht, und liest sie in einem Shot zurück.
Was ist Denormalisierung in DynamoDB?
Denormalisierung in DynamoDB heißt, verwandte Daten auf das Item zu kopieren, das sie liest, sodass eine einzelne Query alles in einem Shot zurückgibt. Weil DynamoDB keine Joins hat, pre-joinst du zur Write-Zeit, statt Tabellen zur Read-Zeit zusammenzunähen. Der Trade-off ist Veraltung — dupliziere nur Werte, die sich selten ändern.
- Keine Joins heißt: Pre-Join zur Write-Zeit. Speichere den verwandten Wert auf dem Item, das ihn liest, sodass eine Query nie einen zweiten Lookup braucht.
- Zwei Varianten. Nested Data in einem komplexen Attribut auf einem Item einbetten, oder einen Wert über viele Items duplizieren.
- Der Fallstrick ist Veraltung. Wenn die Quelle sich ändert, ist jede Kopie falsch, bis du das Update auffächerst. Dupliziere nur Werte, die sich selten ändern.
- Es kauft Reads, nicht Writes. Du tauschst mehr (und vorsichtigere) Writes gegen günstige Single-Request-Reads.
Warum es keine Joins als Fallback gibt
Ein relationales JOIN setzt normalisierte Rows zur Read-Zeit wieder zusammen.
DynamoDB hat keinen Join — eine Query liest eine
und gibt genau zurück, was dort liegt. Nichts näht zwei Tabellen für dich zusammen.
(Auf dem Produktions-Read-Pfad jedenfalls — für Ad-hoc-Audit oder Drift-Check
führt DynoTables SQL Workbench ein echtes JOIN
clientseitig über DynamoDB aus.)
Die Daten müssen also schon für den Read geformt sein. Braucht ein Screen einen Post und den Namen seines Autors, muss dieser Name irgendwo leben, das der Post-Read schon berührt. Das Amazon-Dynamo-Paper von 2007 machte diesen Trade explizit: relationale Features fallen lassen, um vorhersagbare Reads at Scale zu bekommen — den Trade, den DynamoDB heute als Reads im einstelligen Millisekundenbereich liefert.
Muster 1 — mit komplexem Attribut einbetten
DynamoDB-Attribute können nested Maps und Lists halten, nicht nur Skalare. Eine gängige Form der Denormalisierung ist deshalb, ein Child-Objekt direkt in sein Parent-Item zu stopfen, statt ihm ein eigenes Item zu geben.
Ein Post mit Tags und einem kleinen Author-Snapshot, alles auf einem Item:
| PK | SK | author | tags |
|---|---|---|---|
| POST#9f3 | META | {id: U#12, name: "Mara Vance"} | ["dynamodb","aws"] |
Ein GetItem liefert Post, Tags und Author-Block zusammen. Kein zweiter Read.
Das ist gut für Daten, die dem Parent gehören und in der Größe begrenzt sind —
eine Handvoll Tags, ein Author-Snapshot.
Ein einzelnes DynamoDB-Item endet bei maximal 400 KB, Attributnamen und -werte inklusive (Service Quotas). Bette eine unbegrenzte Liste ein (jeder Kommentar auf einem viralen Post) und du sprengst sie.
Muster 2 — einen Wert über Items duplizieren
Der Blog-Fall ist der Lehrbuchfall. Du listest Posts und willst, dass jede Zeile den Display-Namen des Autors zeigt — aber keinen zweiten Read pro Post, um ihn zu holen.
Also schreibst du den Autorennamen auf jedes Post-Item, wenn der Post erstellt wird:
| PK | SK | authorId | authorName | title |
|---|---|---|---|---|
| POST#9f3 | META | U#12 | "Mara Vance" | "Modeling 1:N" |
| POST#a71 | META | U#12 | "Mara Vance" | "Sparse GSIs" |
| POST#b04 | META | U#88 | "Lio Tan" | "Query vs Scan" |
Ein über Posts (z. B. GSI1PK = "POST", oder einer keyed by Author)
rendert die ganze Liste — Titel und Autor — ohne Per-Row-Lookup. begins_with auf
dem Partition Key gibt es nicht; eine Query braucht Partition-Key-Equality, also
kommt die Liste bei Per-Post-Partition-Keys vom GSI, nicht von einer Query über
POST#. Der Autorenname ist denormalisiert: die kanonische Kopie lebt auf
USER#12, und jeder Post trägt seine eigene Kopie.
Der Trade liegt offen. Du hast einen N+1-Read in einen Read verwandelt, zum Preis
von "Mara Vance" an N+1 Stellen.
Einbetten vs. duplizieren — welches
| Einbetten (komplexes Attribut) | Duplizieren (Kopie über Items) | |
|---|---|---|
| Form | Child nested im Parent | derselbe Wert auf vielen Items |
| Am besten für | begrenzte, parent-owned Daten | ein geteilter Wert, den viele Items zeigen |
| Read | ein GetItem | eine Query |
| Update-Kosten | das eine Parent-Item umschreiben | Fan-out zu jeder Kopie |
| Size-Risiko | 400-KB-Item-Cap | keines pro Item |
Greif zu einbetten, wenn das Child nur je mit seinem Parent erscheint. Greif zu duplizieren, wenn viele unabhängige Items denselben geteilten Wert zeigen müssen.
Der Fallstrick: veraltete Kopien
Mara benennt sich in „Mara V.“ um. Du updatest USER#12. Jedes Post-Item sagt
weiterhin "Mara Vance", bis du sie reparierst.
Ein duplizierter Wert zu updaten ist also ein Fan-out-Write, kein One-Liner. Du queryst jedes betroffene Item und schreibst jedes um — idealerweise so geschützt, dass du nur Rows anfasst, die noch den alten Wert halten:
UPDATE POST#9f3
SET authorName = "Mara V."
WHERE authorName = "Mara Vance"
Diesen conditional SET gegen authorName kannst du im
Expression Builder zusammensetzen und die
generierte UpdateExpression und ConditionExpression direkt in deinen Code
kopieren.
Fan-out ist ein Write pro Item. Query den Author-keyed GSI für die Posts dieses Autors, dann führe die Updates aus. Die Sequenz:
Jede Änderung an der Quelle ist eine Query plus ein Write pro Kopie. In DynoTable landet der Fan-out zuerst im Staging-Bereich, als reviewbarer Per-Attribut-Diff pro Item — du siehst jede Kopie, die sich ändern wird, bevor etwas ausgeliefert wird.
Deshalb lautet die Regel: dupliziere nur Werte, die sich selten ändern. Ein Display-Name, ein Plan-Tier, ein Kategorie-Label — fein. Ein Live-Counter oder ein häufig editiertes Feld — nicht; der Fan-out frisst dich lebendig.
Fan-out-Write-Kosten
Jede Kopie, die du updatest, ist ein separat abgerechneter Write. In us-east-1
On-Demand kostet das Updaten von 50 Post-Items nach einem Author-Rename
50 × WCU — typischerweise 1 WCU pro KB pro Item, wenn jede Post-Row ≤
1 KB ist. Die Read-Seite bleibt eine Query; die Write-Seite skaliert mit
der Zahl der Duplikate, die du pflegst. Schätze beide Pfade im
Pricing Calculator.
Wann Normalisierung trotzdem gewinnt
Wenn sich ein Wert oft ändert, oder ein Item von wirklich unvorhersehbaren Mustern gelesen wird, halte es normalisiert und akzeptiere den Extra-Read. Denormalisierung ist eine Optimierung für bekannte, read-heavy Access Patterns — kein Default für überall. Pre-joine die Reads, die du wirklich fährst, und lass den Rest in Ruhe.
Um zu entscheiden, wo diese duplizierten Attribute leben, modelliere zuerst die Access Patterns — siehe Single-Table Design und für die Read-Seite des Trades Query vs Scan.
Lade DynoTable herunter, um eine denormalisierte Tabelle zu
inspizieren, zu sehen, welche Kopien gedriftet sind, und den Fan-out-Update gegen
deine eigenen Daten zu fahren. Sein
SQL Workbench kann sogar die Source of Truth gegen die
Kopien JOINen, um den Drift in einer Query zu finden.