Denormalisierung in DynamoDB
Aus SQL kommend klingt Denormalisierung wie eine Sünde — duplizierte Daten, keine einzige Quelle der Wahrheit. In DynamoDB ist sie der ganze Sinn. Es gibt keine Joins, also kopierst du verwandte Daten auf das Item, das sie braucht und liest sie in einem Rutsch zurück.
Was ist Denormalisierung in DynamoDB?
Denormalisierung in DynamoDB bedeutet, verwandte Daten auf das Item zu kopieren, das sie liest, sodass ein einzelner Query alles in einem Rutsch zurückgibt. Weil DynamoDB keine Joins hat, joinst du zur Schreibzeit vor, statt Tabellen zur Lesezeit zusammenzunähen. Der Tausch ist Staleness — dupliziere nur Werte, die sich selten ändern.
- Keine Joins bedeutet, du joinst zur Schreibzeit vor. Speichere den verwandten Wert auf dem Item, das ihn liest, sodass ein Query nie einen zweiten Lookup braucht.
- Zwei Spielarten. Bette verschachtelte Daten in einem komplexen Attribut auf einem Item ein, oder dupliziere einen Wert über viele Items.
- Der Footgun ist Staleness. Wenn sich die Quelle ändert, ist jede Kopie falsch, bis du das Update auffächerst. Dupliziere nur Werte, die sich selten ändern.
- Es erkauft Lesevorgänge, keine Schreibvorgänge. Du tauschst mehr (und sorgfältigere) Schreibvorgänge gegen günstige Lesevorgänge mit einer einzigen Anfrage.
Warum es keine Joins gibt, auf die du zurückgreifen kannst
Ein relationaler JOIN setzt normalisierte Zeilen zur Lesezeit wieder zusammen.
DynamoDB hat keinen Join — ein Query liest eine Item Collection und gibt genau
das zurück, was dort gespeichert ist. Nichts näht zwei Tabellen für dich zusammen.
Die Daten müssen also bereits für den Lesevorgang geformt sein. Wenn ein Screen einen Beitrag und den Namen seines Autors braucht, muss dieser Name irgendwo liegen, wo der Beitrags-Lesevorgang ohnehin hinkommt. Das Amazon-Dynamo-Paper von 2007 machte diesen Tausch explizit: Verzichte auf relationale Features, um vorhersehbare Lesevorgänge im einstelligen Millisekundenbereich bei großer Skalierung zu bekommen.
Muster 1 — einbetten mit einem komplexen Attribut
DynamoDB-Attribute können verschachtelte Maps und Listen halten, nicht nur Skalare. Eine gängige Form der Denormalisierung ist also, ein Kind-Objekt direkt in sein Eltern-Item zu stopfen, statt ihm ein eigenes Item zu geben.
Ein Beitrag mit seinen Tags und einem kleinen Autoren-Snapshot, alles auf einem Item:
| PK | SK | author | tags |
|---|---|---|---|
| POST#9f3 | META | {id: U#12, name: "Mara Vance"} | ["dynamodb","aws"] |
Ein GetItem liefert den Beitrag, die Tags und den Autorenblock zusammen. Kein
zweiter Lesevorgang. Das ist großartig für Daten, die der Eltern-Entität
gehören und in der Größe begrenzt sind — eine Handvoll Tags, ein
Autoren-Snapshot.
Die Grenze, die du beachten musst: Ein einzelnes DynamoDB-Item ist auf 400 KB gedeckelt, Attributnamen und -werte inklusive (Service Quotas). Bette eine unbegrenzte Liste ein (jeden Kommentar zu einem viralen Beitrag), und du überschreitest sie.
Muster 2 — einen Wert über Items duplizieren
Der Blog-Fall ist der Lehrbuchfall. Du listest Beiträge und willst, dass jede Zeile den Anzeigenamen des Autors zeigt — aber du willst keinen zweiten Lesevorgang pro Beitrag, um ihn zu holen.
Also schreibst du den Namen des Autors auf jedes Beitrags-Item, wenn der Beitrag 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" |
Jetzt rendert Query PK begins_with "POST#" (oder ein GSI über Beiträge) die ganze
Liste — Titel und Autor — ohne Lookup pro Zeile. Der Autorenname ist
denormalisiert: Die kanonische Kopie liegt auf USER#12, und jeder Beitrag
trägt seine eigene Kopie.
Der Tausch liegt direkt vor dir. Du hast einen N+1-Lesevorgang in einen
Lesevorgang verwandelt, zum Preis, "Mara Vance" an N+1 Stellen zu halten.
Einbetten vs. duplizieren — welches
| Einbetten (komplexes Attribut) | Duplizieren (Kopie über Items) | |
|---|---|---|
| Form | Kind verschachtelt in Eltern | gleicher Wert auf vielen Items |
| Am besten für | begrenzte, eltern-eigene Daten | ein geteilter Wert, den viele Items zeigen |
| Lesevorgang | ein GetItem | ein Query |
| Update-Kosten | das eine Eltern-Item neu schreiben | auf jede Kopie auffächern |
| Größenrisiko | 400-KB-Item-Limit | keines pro Item |
Greife zum Einbetten, wenn das Kind nur je mit seinem Elternteil auftaucht. Greife zum Duplizieren, wenn viele unabhängige Items denselben geteilten Wert zeigen müssen.
Der Footgun: veraltete Kopien
Hier ist der Teil, der beißt. Mara benennt sich in „Mara V." um. Du aktualisierst
USER#12. Jedes Beitrags-Item sagt immer noch "Mara Vance", bis du sie
reparierst.
Das Aktualisieren eines duplizierten Werts ist also ein Fan-out-Schreibvorgang, kein Einzeiler. Du fragst jedes betroffene Item ab und schreibst jedes neu — idealerweise abgesichert, sodass du nur Zeilen anfasst, die noch den alten Wert halten:
UPDATE POST#9f3
SET authorName = "Mara V."
WHERE authorName = "Mara Vance"
Du kannst dieses bedingte SET gegen authorName im
Expression Builder zusammensetzen und die
generierte UpdateExpression und ConditionExpression direkt in deinen Code
kopieren.
Das Fan-out selbst ist ein Schreibvorgang pro Item: Frage die Beiträge des Autors ab, dann setze die Updates ab. Die Reihenfolge:
Die Kosten des Duplizierens von Daten: Jede Änderung an der Quelle ist ein Query plus ein Schreibvorgang pro Kopie.
Deshalb lautet die Regel: Dupliziere nur Werte, die sich selten ändern. Ein Anzeigename, eine Plan-Stufe, ein Kategorie-Label — okay. Ein Live-Counter oder ein häufig bearbeitetes Feld — nicht; das Fan-out frisst dich bei lebendigem Leibe.
Wann Normalisierung trotzdem gewinnt
Wenn sich ein Wert häufig ändert, oder ein Item nach wirklich unvorhersehbaren Mustern gelesen wird, halte ihn normalisiert und nimm den zusätzlichen Lesevorgang in Kauf. Denormalisierung ist eine Optimierung für bekannte, leselastige Zugriffsmuster — kein Standard, den man überall anwendet. Joine die Lesevorgänge vor, die du tatsächlich ausführst, und lass den Rest in Ruhe.
Um zu entscheiden, wo diese duplizierten Attribute liegen, modelliere zuerst die Zugriffsmuster — siehe Single-Table-Design und, für die Leseseite des Tauschs, Query vs. Scan.
Lade DynoTable herunter, um eine denormalisierte Tabelle zu inspizieren, zu erkennen, welche Kopien abgedriftet sind, und das Fan-out-Update gegen deine eigenen Daten auszuführen.