Profi7 Min. Lesezeit

Key Overloading in DynamoDB

Aus SQL kommend bedeutet eine Spalte für immer dasselbe: orders.created_at ist immer ein Datum, users.email immer eine E-Mail. Key Overloading wirft das über Bord. Du gibst Partitions- und generische Namen — pk, sk — und lässt jeden Item-Typ eine andere Bedeutung hineingießen. Eine Tabelle, viele Entitäten, eine Form.

Was ist Key Overloading in DynamoDB?

Key Overloading heißt: viele Entitätstypen in einer Tabelle unter generischen Schlüsselnamen wie pk/sk ablegen und den Typ im Wert kodieren (USER#u_3001, INVOICE#2026-0014). Der Attributname bleibt neutral, sodass Nutzer, Rechnungen und Events sich eine Partition teilen; der Wert trägt den Typ, und ein Präfix im Sortierschlüssel lässt ein einzelnes Query jede Entität per begins_with herausschneiden.

  • Generische Schlüsselnamen, typisierte Werte. Nenn deine Schlüssel pk/sk und steck den Entitätstyp in den Wert: pk = "TENANT#acme", sk = "USER#u_3001". Der Name ist dumm; der Wert trägt den Typ.
  • Genau das lässt Single-Table Design funktionieren. Ohne Overloading ist eine geteilte Tabelle nur eine Kramschublade. Mit ihm sitzt jede Entität in einer Partition, die du per Query lesen kannst.
  • begins_with ist der Gewinn. Ein Typ-Präfix auf dem Sortierschlüssel lässt ein Query eine ganze Entität oder ein Stück davon holen, ohne Scan und ohne Filter.
  • Der Preis: Lesbarkeit. Ein roher pk/sk-Dump sagt dir gar nichts. Du brauchst einen Viewer, der die Präfixe dekodiert, sonst kneifst du die Augen über Strings zusammen.

Warum generische Namen echte schlagen

DynamoDB gibt dir höchstens zwei Schlüsselattribute pro Tabelle, und ein Query kann nur einen einzigen Partitionsschlüssel ansteuern. Nennst du deinen Schlüssel also userId, können nur Nutzer-Items sauber in dieser Tabelle liegen — alles andere muss eine userId vortäuschen oder in eine eigene Tabelle ziehen.

Overloading umgeht das. Ein neutraler Name wie pk legt sich auf keine Entität fest, ein Nutzer, eine Rechnung und ein Audit-Event können sich also dasselbe Schlüsselattribut und dieselbe Tabelle teilen. Der Wert, nicht der Attributname, sagt, was das Item ist.

Das ist der Schritt, der Single-Table Design von Theorie in etwas verwandelt, das du tatsächlich abfragen kannst. Die geteilte Tabelle ist der Behälter; Overloading ist das, was verschiedene Entitäten darin koexistieren lässt.

Ein Multi-Tenant-Beispiel

Sagen wir, du betreibst ein SaaS-Billing-Produkt. Jeder Mandant hat Mitglieder, Rechnungen und eine Audit-Spur. Statt drei Tabellen packst du alles in eine und überlädst die Schlüssel:

pkskattributes
TENANT#acmeMETAname="Acme Inc", plan="team"
TENANT#acmeUSER#u_3001email, role="admin"
TENANT#acmeUSER#u_3002email, role="member"
TENANT#acmeINVOICE#2026-0014amount_cents, status="paid"
TENANT#acmeINVOICE#2026-0015amount_cents, status="open"
TENANT#acmeEVENT#2026-06-23T09:12Zactor="u_3001", action="invite"

Jede Zeile teilt sich pk = "TENANT#acme", sie bilden also eine — alle am selben Ort, alle in einem einzigen Partitions-Read erreichbar.

Partition: TENANT#acmesk: METAsk: USER#u_3001sk: INVOICE#2026-0015sk: EVENT#2026-06-23T09:12ZEine Query

Die eigentliche Arbeit macht das Präfix des Sortierschlüssels. Es gruppiert Entitäten und ordnet sie.

Die überladene Collection abfragen

Weil der Typ im Präfix des Sortierschlüssels steckt, schneidet begins_with die Partition nach Entität auf, ganz ohne Scan:

Query pk = "TENANT#acme"  -- the entire tenant, every type
Query pk = "TENANT#acme" AND begins_with(sk, "USER#")  -- just members
Query pk = "TENANT#acme" AND begins_with(sk, "INVOICE#")  -- just invoices

Du zahlst nur für die Items, auf die die Bedingung passt, nicht für die ganze Partition — das Gegenteil eines gefilterten Scan, bei dem du dafür zahlst, Zeilen zu lesen, die du danach wegwirfst. AWS nennt das eine Key Condition; sie läuft auf den Schlüsseln, bevor Daten die Partition verlassen.

Wenn du diese begins_with-Bedingung von Hand baust, achte auf die Typ-Tags — ein verirrtes USERS# statt USER# liefert lautlos nichts zurück. Der Expression Builder erzeugt die KeyConditionExpression und die ExpressionAttributeValues-Map, damit die Präfixe zu dem passen, was du tatsächlich geschrieben hast.

Überlade auch den Index

Derselbe Trick funktioniert bei einem . Gib ihm generische Schlüsselnamen — gsi1pk, gsi1sk — und lass jede Entität hineinschreiben, was sie braucht. Ein Index beantwortet dann Muster, die die Basistabelle nicht kann.

pkskgsi1pkgsi1sk
TENANT#acmeINVOICE#2026-0015STATUS#open2026-06-30
TENANT#acmeINVOICE#2026-0014STATUS#paid2026-06-12
TENANT#betaINVOICE#2026-0099STATUS#open2026-06-25

Jetzt listet Query gsi1 WHERE gsi1pk = "STATUS#open" jede offene Rechnung über alle Mandanten hinweg auf, nach Fälligkeitsdatum sortiert — eine partitionsübergreifende Sicht, die die mandantengebundenen Schlüssel der Basistabelle nie bedienen könnten. Eine andere Entität kann gsi1 mit eigener Bedeutung wiederverwenden (etwa gsi1pk = "ROLE#admin"), ein Index deckt also mehrere Reads ab. Denk nur daran, dass ein GSI letztendlich konsistent ist — seine Writes hinken der Basistabelle hinterher.

In DynoTable umsetzen

Rohe überladene Schlüssel sind unangenehm zu lesen: INVOICE#2026-0015 und EVENT#2026-06-23T09:12Z verschwimmen in einer flachen Liste. Ein Viewer, der nach Partition gruppiert und die Präfixe herausstellt, macht aus der Kramschublade wieder Entitäten.

DynoTable durchsucht die Item Collection eines Mandanten — META-, USER-, INVOICE- und EVENT-Items unter einem einzigen überladenen Partitionsschlüssel gruppiert.
DynoTable durchsucht die Item Collection eines Mandanten — META-, USER-, INVOICE- und EVENT-Items unter einem einzigen überladenen Partitionsschlüssel gruppiert.

Fallstricke

  • Leg Trennzeichen einmal fest und ändere sie nie. # ist die Konvention. # und : über Entitäten hinweg zu mischen bricht begins_with auf eine Weise, vor der dich nichts warnt.
  • Überlade keine Werte, die Bereichsarithmetik brauchen. Ein Sortierschlüssel INVOICE#2026-0015 sortiert lexikalisch, nicht numerisch — fülle IDs und nimm ISO-8601-Datumsangaben, damit die String-Reihenfolge der gemeinten Reihenfolge entspricht.
  • Reserviere den Präfix-Namensraum. Zwei Entitätstypen, die beide mit USER anfangen (etwa USER# und USERGROUP#), kollidieren unter begins_with(sk, "USER"). Mach Präfixe schon ab dem ersten Zeichen eindeutig.
  • Plane den Read vor den Schlüsseln. Overloading bedient Zugriffsmuster, die du aufgezählt hast. Kennst du deine Reads noch nicht, lies zuerst Single-Table Design — die Schlüssel kommen nach den Abfragen.

Zeichne eine Partition auf und lade dann DynoTable herunter, um deine eigenen überladenen Schlüssel zu durchsuchen und zuzusehen, wie ein Query einen ganzen Mandanten auf einmal zurückholt.

Kosten einer Query auf einer überladenen Partition

Alle Mitglieder unter TENANT#acme mit begins_with(sk, "USER#") aufzulisten liest nur Nutzerzeilen — keine Rechnungen, keine Events —, weil die Key Condition filtert, bevor Daten die Partition verlassen. Bei einem Mandanten mit 200 Nutzern (je 2 KB) und 5.000 Audit-Events (je 1 KB) berührt diese Abfrage ~400 KB (~100 letztendlich konsistente RCU). Ein Scan über die ganze Tabelle, um Nutzer zu finden, würde jedes Item jedes Mandanten abrechnen.

Füg repräsentative überladene Items in den Item-Size-Rechner ein und schätz die Listen-Abfragen dann im Preisrechner.

Mit dem Single-Table-Tool entwerfen

Gib Entitäten (Tenant, User, Invoice, Event) und Zugriffsmuster („Nutzer eines Mandanten auflisten", „offene Rechnungen über alle Mandanten") in das Single-Table-Design-Tool ein. Es schlägt pk/sk-Vorlagen und GSI-Schlüssel vor, die zu den Overload-Präfixen passen, die du in Produktion verwenden wirst — bevor du CloudFormation festschreibst.

Queries aus Mustern erzeugen

Sobald die Präfixe stehen, baust du die Key Conditions im Expression Builder und exportierst ein paginiertes Programm aus dem Query Builder. Präfix-Tippfehler (USER# statt USERS#) liefern leere Ergebnisse ohne Fehler — generierte Expressions verringern genau diesen stillen Fehlermodus.

Register der Entitätstyp-Präfixe

Pfleg eine kurze interne Tabelle, auf die Entwickler zurückgreifen können:

EntitätSortier-PräfixBeispiel-SKQuery-Ausschnitt
Tenant-MetaMETAMETAEinzel-Item-Get
UserUSER#USER#u_3001begins_with(sk, "USER#")
InvoiceINVOICE#INVOICE#2026-0015begins_with(sk, "INVOICE#")
EventEVENT#EVENT#2026-06-23T09:12Zzeitlich sortiertes Ende mit absteigendem Read

Neue Entitätstypen müssen Präfixe wählen, die nicht unter begins_with bestehender Präfixe kollidieren — USER# und USERGROUP# treffen beide auf begins_with(sk, "USER") zu, sofern du nicht sorgfältig verlängerst oder mit einem Trennzeichen abgrenzt.

Aktualisiert