Fortgeschritten9 Min. Lesezeit

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 JOIN als Operator. Hatte es nie.
  • PartiQLs SELECT ist nur für eine einzige Tabelle — die Grammatik lautet buchstäblich SELECT … FROM {{table}}[.{{index}}], und wenn du es auf zwei Tabellen richtest, liefert es ValidationException: 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 DESC

ist 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 supported

Wenn 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 eine Customers-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.

Aktualisiert