DynamoDB JOIN: So verbindest du Tabellen (und warum du es meist nicht kannst)
In DynamoDB gibt es kein JOIN. Die API hat keinen Join-Operator, das Datenmodell
hat keine Fremdschlüssel und — der Teil, der die meisten überrascht — auch
PartiQL, die SQL-ähnliche Abfrageschicht, fügt keines hinzu. Ein PartiQL-SELECT
liest genau eine Tabelle.
Wenn du von einer relationalen Datenbank kommst, ist das die erste Wand, gegen die du läufst. Dieser Leitfaden erklärt, warum die Wand da ist, die vier Dinge, die Entwickler stattdessen tun, den einen Fall, in dem du tatsächlich ein echtes Join brauchst — und wie du eines ausführst.
Kann DynamoDB Joins?
Nein. DynamoDB kann keine Tabellen verbinden — weder über die Low-Level-API (GetItem /
Query / Scan / BatchGetItem) noch über PartiQL noch über irgendeinen eingebauten
Query-Planner, denn es gibt keinen. Jeder Read bildet auf eine einzelne Tabelle oder
einen ihrer Indizes ab; das Kombinieren zweier Tabellen über einen passenden Schlüssel
ist etwas, das du in deiner App erledigst, nachdem DynamoDB die Items zurückgegeben hat,
niemals innerhalb davon.
- DynamoDB hat kein
JOINals Operator. Hatte es nie. - PartiQLs
SELECTist nur für eine einzige Tabelle — die Grammatik lautet buchstäblichSELECT … FROM {{table}}[.{{index}}], und wenn du es auf zwei Tabellen richtest, liefert esValidationException: Only Select from a Single Table or index supported. - Die Lösung, die AWS empfiehlt, ist, kein Join zu brauchen: denormalisieren oder Single-Table-Design nutzen, damit verwandte Items in einer Partition liegen, die du in einer einzigen Anfrage abrufst.
- Für den echten tabellenübergreifenden / ad-hoc Fall verbindest du außerhalb von DynamoDB — in deiner App oder mit einem Tool, das es für dich erledigt.
Warum DynamoDB keine Joins hat
Ein SQL-JOIN verlangt von der Datenbank, mehrere Tabellen zu lesen und sie zur
Abfragezeit zusammenzusetzen. AWS' eigener
Leitfaden zur Modellierung relationaler Daten
benennt die Kosten: Eine Abfrage wie
SELECT * FROM Orders
INNER JOIN Order_Items ON Orders.Order_ID = Order_Items.Order_ID
INNER JOIN Products ON Products.Product_ID = Order_Items.Product_ID
ORDER BY Quantity_on_Hand DESCist flexibel, aber „jeder Join in der Abfrage erhöht die Laufzeitkomplexität der Abfrage, weil die Daten jeder Tabelle bereitgestellt und dann zusammengesetzt werden müssen." Dieser Aufwand ist unbeschränkt — seine Kosten hängen von den Daten ab, nicht von der Abfrage — was genau die Eigenschaft ist, die DynamoDB verweigert.
Also hat AWS die Beschränkung eingebaut. DynamoDB ist, in ihren Worten, „darauf
ausgelegt, beide [CPU- und Netzwerk-]Beschränkungen zu minimieren, indem es JOINs
eliminiert (und die Denormalisierung von Daten fördert) und die
Datenbankarchitektur so optimiert, dass eine Anwendungsabfrage mit einer einzigen
Anfrage an ein Item vollständig beantwortet wird." Das sind die Eigenschaften, die
einstellige Millisekunden-Latenz bei jeder Größenordnung erkaufen: Die Laufzeitkosten
eines DynamoDB-Reads sind konstant, unabhängig von der Tabellengröße. Es gibt keine
Join-Engine und kein Fremdschlüssel-Konzept, gegen das geplant werden könnte — by
design.
„Aber PartiQL ist doch SQL, das macht sicher Joins?"
Nein. PartiQL gibt dir SELECT / INSERT / UPDATE / DELETE-Syntax über
DynamoDB, aber es ist SQL-kompatibel, nicht SQL. Die
offizielle SELECT-Grammatik
lautet:
SELECT {{expression}} [, ...]
FROM {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]FROM nimmt eine Tabelle (optional einen ihrer Indizes). Es gibt keine zweite
FROM-Tabelle, kein JOIN, keine Subquery, keine CTE. Richte PartiQL auf zwei
Tabellen und DynamoDB lehnt es ab
(gemeldet auf AWS re:Post):
ValidationException: Only Select from a Single Table or index supportedWenn du die vollständige Begründung willst, warum PartiQL wie SQL aussieht, sich aber nicht so verhalten kann, siehe PartiQL vs SQL.
Die 4 Workarounds, die Entwickler tatsächlich nutzen
1. Denormalisieren (die Daten hineinkopieren)
Speichere die Felder, gegen die du sonst joinen würdest, direkt am Item. Eine Order
trägt einen Snapshot von customerName und shippingAddress statt einer
customerId, die du später auflösen würdest. Ein Read, kein Join.
Der Preis ist Fan-out zur Schreibzeit: Wenn sich die Quelle ändert, aktualisierst du jede Kopie (typischerweise über einen DynamoDB-Streams-Handler). Du tauschst Lese-Komplexität gegen Schreib-Komplexität — meist ein guter Tausch für eine leselastige App.
2. Single-Table-Design (Pre-Join in der Partition)
Lege verwandte Entitäten in eine Tabelle unter einen gemeinsamen Partition Key,
sodass eine Item-Collection das verbundene Ergebnis ist. Ein Kunde und all seine
Bestellungen teilen sich PK = "CUSTOMER#42"; eine Query liefert das Kunden-Item
plus jedes Bestell-Item — der „Join" ist bereits zur Schreibzeit passiert.
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (der Kunde)
→ CUSTOMER#42 / ORDER#1001 (eine Bestellung)
→ CUSTOMER#42 / ORDER#1002 (eine Bestellung)
Das ist die kanonische DynamoDB-Antwort auf 1:n-Beziehungen. Vollständige Durchgehung in Single-Table-Design.
3. Join auf Anwendungsseite (zwei Reads, im Code zusammenfügen)
Lies aus Tabelle A, nimm die zurückgegebenen Schlüssel, lies aus Tabelle B und führe die beiden Ergebnismengen in deiner Anwendung zusammen. Es ist die relationale Join-Logik — nur in deinem Code statt in der Datenbank ausgeführt:
// „Hole jede Bestellung mit dem Kundennamen" — der manuelle Join.
const {Items: orders} = await ddb.query({TableName: 'Orders' /* … */});
const customers = await Promise.all(
orders.map((o) => ddb.getItem({TableName: 'Customers', Key: {id: o.customerId}}))
);
const joined = orders.map((o, i) => ({
...o,
customerName: customers[i].Item?.name
}));In Ordnung bei kleinem Fan-out. Bei vielen Bestellungen wird es zu einem
N+1-Problem — ein Read, um Bestellungen aufzulisten, dann ein Read pro Bestellung
— was langsam ist und Lesekapazität verbrennt. BatchGetItem (nächster Punkt) bündelt
diese zweite Welle in einen Roundtrip.
4. BatchGetItem (ein Roundtrip, mehrere Tabellen)
BatchGetItem
kommt dem „zwei Tabellen auf einmal anfassen" am nächsten, was die API erlaubt: Eine
Anfrage liefert „die Attribute eines oder mehrerer Items aus einer oder mehreren
Tabellen", bis zu 100 Items oder 16 MB pro Aufruf, je nachdem, was zuerst
erreicht wird. Es schneidet die Roundtrips eines App-seitigen Joins — aber es ist
kein Join. Du „identifizierst angeforderte Items per Primary Key"; es gibt keine
ON-Bedingung und kein relationales Matching. Du musst die Schlüssel weiterhin vorab
kennen und die Antworten selbst zusammenfügen.
Wenn ein echtes JOIN unvermeidbar ist
Die vier Workarounds decken Produktions-Lesepfade gut ab. Wo sie scheitern, ist die ad-hoc, explorative, analytische Abfrage — die, für die du nicht modelliert hast:
- „Welche Kunden in der EU haben letzten Monat eine Bestellung über 500 $
aufgegeben?" über eine
Orders-Tabelle und eineCustomers-Tabelle. - Eine einmalige Datenqualitätsprüfung, die zwei Entitätstypen verbindet.
- Reporting und Aggregate (
GROUP BY,SUM,COUNT) — wofür DynamoDB überhaupt keinen Operator hat.
Das sind genau die Abfragen, die du nicht in eine Partition vorab backen kannst, weil
du per Definition nicht wusstest, dass du sie stellen würdest. Der relationale
Instinkt — ein JOIN schreiben — ist hier der richtige. DynamoDB kann ihn nur nicht
nativ bedienen, und PartiQL auch nicht.
Die übliche schwergewichtige Antwort ist, nach S3 zu exportieren und mit Athena abzufragen oder in ein Warehouse zu pipen. Das ist korrekt für echte Analytik im großen Maßstab, aber es ist viel Klempnerei für eine Frage, die du jetzt beantwortet haben willst, gegen deine Live-Tabelle.
Ein echtes JOIN mit DynoTables SQL Workbench ausführen
DynoTable ist ein Desktop-DynamoDB-Client, dessen SQL Workbench
tatsächliches SQL ausführt — einschließlich JOIN, GROUP BY und
Aggregatfunktionen — über deine DynamoDB-Tabellen. Es liest die Items über die
normale DynamoDB-API und führt dann die relationalen Teile der Abfrage im Client aus.
So kannst du schreiben:
SELECT c.name, SUM(o.total) AS spend
FROM Customers c
JOIN Orders o ON o.customerId = c.id
WHERE c.region = 'EU'
GROUP BY c.name
HAVING SUM(o.total) > 500— und bekommst eine Ergebnismenge, gegen Tabellen, die keine definierte Beziehung
haben, und eine Query-Engine, die kein JOIN-Schlüsselwort hat.
Die ehrliche Einschränkung — „innerhalb von DynamoDBs Zugriffsmuster-Regeln": Die
Workbench liest weiterhin über DynamoDB, also ist ein unbeschränkter Join ein
unbeschränkter Read. Die schnellsten Abfragen sind die, bei denen die WHERE-Klausel
(oder das ON-Attribut des Joins) auf mindestens einer Seite einen Partition Key
oder ein GSI trifft, sodass DynamoDB eine Query statt eines
vollständigen Tabellen-Scans ausführt, bevor der Join läuft.
Die Workbench hebt die Beschränkungen in diesem Leitfaden nicht auf — sie lässt dich
nur die SQL-Frage stellen, statt das Zusammenfügen selbst von Hand zu schreiben, und
sagt dir, was sie darunter tut.
Es ist das einzige „ja, du kannst joinen", das tatsächlich stimmt: PartiQL und AWS'
eigene
NoSQL Workbench
— deren Operation Builder auf Single-Table-Data-Plane-Operationen (Query / Scan /
GetItem) beschränkt ist — bleiben beide an der Single-Table-Wand stehen, ebenso wie
die meisten anderen GUI-Clients. Sieh dir an, wie DynoTable als
DynamoDB-GUI abschneidet.
FAQ
Unterstützt PartiQL JOIN?
Nein. PartiQLs SELECT liest eine einzige Tabelle (oder einen ihrer Indizes). Eine
tabellenübergreifende Abfrage liefert ValidationException: Only Select from a Single Table or index supported. Dieselbe Wand wie beim Rest der API.
Kann man zwei DynamoDB-Tabellen in einer Abfrage verbinden?
Nicht nativ. Die DynamoDB-API hat kein Statement, das zwei Tabellen liest und sie
über einen Schlüssel matcht. BatchGetItem kann Items aus mehreren Tabellen in einer
Anfrage lesen, hat aber keine ON-Bedingung — es liefert die Items zurück, die du per
Primary Key benannt hast, und überlässt dir das Matching. Ein echtes JOIN … ON …
passiert nur außerhalb von DynamoDB: in deiner App oder in DynoTables SQL Workbench.
Kann man eine Tabelle mit ihrem GSI verbinden?
Nein — ein Global Secondary Index ist keine separate Tabelle,
mit der du joinst; es ist eine alternative Schlüsselsicht auf dieselben Items. Du
Queryst in einem gegebenen SELECT entweder die Tabelle oder den Index, nicht
beide zusammengejoint. Ein GSI lässt dich Items über einen anderen Schlüssel
erreichen, was oft die Notwendigkeit eines Joins von vornherein beseitigt.
Kann man über zwei AWS-Konten hinweg joinen (oder zwei Tabellen in verschiedenen
Konten)?
Nicht nativ, und auch nicht mit BatchGetItem — eine einzelne Anfrage kann keine
Credentials überspannen, und es gibt kein kontenübergreifendes Join-Primitiv. Du
würdest jede Tabelle mit den Credentials ihres eigenen Kontos lesen und die Ergebnisse
in deiner Anwendung oder in einem Tool wie DynoTables Workbench verbinden.
Ist Denormalisierung wirklich besser als ein Join? Für DynamoDBs Ziel-Workload — vorhersehbare, hochvolumige Reads — ja. Du verlagerst die Kosten auf die Schreibzeit (und akzeptierst etwas Datenduplikation) im Tausch gegen Single-Request-Reads, die flach skalieren. Der Single-Table-Design-Leitfaden behandelt die Trade-offs.
Die Schlüssel und Bedingungen für diese Reads von Hand zu bauen ist fummelig — der
Expression Builder generiert die
KeyConditionExpression- / FilterExpression-Syntax für dich, und
DynoTable führt das echte SQL aus, wenn ein Workaround nicht reicht.