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/skund 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
Querylesen kannst. begins_withist der Gewinn. Ein Typ-Präfix auf dem Sortierschlüssel lässt einQueryeine ganze Entität oder ein Stück davon holen, ohneScanund 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:
| pk | sk | attributes |
|---|---|---|
| TENANT#acme | META | name="Acme Inc", plan="team" |
| TENANT#acme | USER#u_3001 | email, role="admin" |
| TENANT#acme | USER#u_3002 | email, role="member" |
| TENANT#acme | INVOICE#2026-0014 | amount_cents, status="paid" |
| TENANT#acme | INVOICE#2026-0015 | amount_cents, status="open" |
| TENANT#acme | EVENT#2026-06-23T09:12Z | actor="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.
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.
| pk | sk | gsi1pk | gsi1sk |
|---|---|---|---|
| TENANT#acme | INVOICE#2026-0015 | STATUS#open | 2026-06-30 |
| TENANT#acme | INVOICE#2026-0014 | STATUS#paid | 2026-06-12 |
| TENANT#beta | INVOICE#2026-0099 | STATUS#open | 2026-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.

Fallstricke
- Leg Trennzeichen einmal fest und ändere sie nie.
#ist die Konvention.#und:über Entitäten hinweg zu mischen brichtbegins_withauf eine Weise, vor der dich nichts warnt. - Überlade keine Werte, die Bereichsarithmetik brauchen. Ein Sortierschlüssel
INVOICE#2026-0015sortiert 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
USERanfangen (etwaUSER#undUSERGROUP#), kollidieren unterbegins_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ät | Sortier-Präfix | Beispiel-SK | Query-Ausschnitt |
|---|---|---|---|
| Tenant-Meta | META | META | Einzel-Item-Get |
| User | USER# | USER#u_3001 | begins_with(sk, "USER#") |
| Invoice | INVOICE# | INVOICE#2026-0015 | begins_with(sk, "INVOICE#") |
| Event | EVENT# | EVENT#2026-06-23T09:12Z | zeitlich 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.


