DynamoDB JOIN: Tabellen joinen
Es gibt kein JOIN in DynamoDB. Die API hat keinen Join-Operator, das Datenmodell
keine Foreign Keys, und — der Teil, der die meisten überrascht — ,
der SQL-angehauchte Query-Layer, fügt auch keines hinzu. Ein
PartiQL-SELECT liest genau eine Tabelle.
Kommst du von einer relationalen Datenbank, ist das die erste Wand, gegen die du läufst. Dieser Guide behandelt, warum die Wand da ist, welche vier Dinge Devs stattdessen tun, den einen Fall, in dem du wirklich einen echten Join brauchst — und wie du einen fährst.
Kann DynamoDB Joins?
Nein. DynamoDB kann keine Tabellen joinen — nicht über die Low-Level-API
(GetItem / Query / Scan / BatchGetItem), nicht über
und auch nicht über einen eingebauten Query-Planner, denn es gibt keinen. Jeder
Read zielt auf eine Tabelle oder einen ihrer Indexes; zwei Tabellen über einen
passenden Key zu kombinieren, ist etwas, das du in deiner App machst, nachdem
DynamoDB die Items zurückgegeben hat, nie darin.
- DynamoDB hat keinen
JOIN-Operator. Hatte es nie. - PartiQLs
SELECTist Single-Table only — die Grammatik lautet wörtlichSELECT … FROM {{table}}[.{{index}}], und richtest du es auf zwei Tabellen, bekommst duValidationException: Only select from a single table or index is supported. - Die Lösung, die AWS empfiehlt, lautet: keinen Join brauchen — oder Single-Table Design nutzen, damit verwandte Items in einer Partition liegen, die du mit einem einzigen Request holst.
- Für den echten Cross-Table-/Ad-hoc-Fall joinst 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
Query-Zeit zusammenzusetzen. AWS' eigener
Guide zum Modellieren relationaler Daten
benennt die Kosten: Eine Query wie diese
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
INNER JOIN Inventories ON Products.Product_ID = Inventories.Product_ID
ORDER BY Quantity_on_Hand DESCist flexibel, aber "each join in the query increases the runtime complexity of the query because the data for each table must stage and then be assembled." Diese Arbeit ist unbegrenzt — ihre Kosten hängen von den Daten ab, nicht von der Query —, und genau diese Eigenschaft will DynamoDB nicht haben.
Also hat AWS die Constraint eingebaut. DynamoDB ist, in ihren Worten,
"built to minimize both [CPU and network] constraints by eliminating JOINs
(and encouraging denormalization of data) and optimizing the database
architecture to fully answer an application query with a single request to an
item." Genau das sind die Eigenschaften, die einstellige Millisekunden-Latenz bei
jeder Scale erkaufen: Die Laufzeitkosten eines DynamoDB-Reads sind konstant,
unabhängig von der Tabellengröße. Es gibt keine Join-Engine und kein
Foreign-Key-Konzept, gegen das sich planen ließe — mit Absicht.
„Aber PartiQL ist SQL, sicher joined es?“
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 Indexes). Es gibt keine
zweite FROM-Tabelle, kein JOIN, keine Subquery, kein CTE. Wir haben alle
drei gegen DynamoDB laufen lassen, um zu sehen, wie genau jede scheitert.
Ein explizites JOIN:
SELECT o.pk FROM "Orders" o JOIN "Customers" c ON o.customerId = c.pkValidationException: Only select from a single table or index is supported.Zwei Tabellen in FROM — dieselbe Ablehnung: die Engine verweigert also nicht
das Keyword JOIN, sondern die zweite Tabelle:
SELECT * FROM "Orders", "Customers"ValidationException: Only select from a single table or index is supported.Eine Subquery scheitert anders, was beim Debuggen hilfreich ist. PartiQL
kommt gar nicht bis zur Multi-Table-Prüfung — es weist den IN-Operanden vorher
zurück, sodass die Meldung Tabellen nie erwähnt:
SELECT * FROM "Orders" WHERE customerId IN (SELECT pk FROM "Customers")ValidationException: IN operator must have a left hand argument of type Variable
Reference and right hand argument of type Seq with at least one memberKeine Formulierung bringt dir ein Join. Die ersten beiden sterben an der zweiten Tabelle, die dritte noch früher am Operanden.
Willst du die vollständige Begründung, warum PartiQL wie SQL aussieht, sich aber nicht so verhalten kann, siehe PartiQL vs SQL.
Die 4 Workarounds, die Devs wirklich nutzen
1. Denormalisieren (Daten rein kopieren)
Speichere die Felder, 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 müsstest. Ein Read, kein Join.
Der Preis ist der Fan-out zur Write-Zeit. Ändert sich die Quelle, aktualisierst du jede Kopie (typischerweise über einen -Handler). Du tauschst Read-Komplexität gegen Write-Komplexität — für eine leselastige App meist ein guter Tausch.
2. Single-Table Design (Pre-Join in der Partition)
Leg verwandte Entities in eine Tabelle unter einen gemeinsamen Partition Key,
sodass eine das gejointe Result ist. Ein
Kunde und alle seine Orders teilen sich PK = "CUSTOMER#42"; eine Query
liefert das Kunden-Item plus jedes Order-Item — der „Join“ ist schon zur
Write-Zeit passiert.
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (the customer)
→ CUSTOMER#42 / ORDER#1001 (an order)
→ CUSTOMER#42 / ORDER#1002 (an order)
Das ist die kanonische DynamoDB-Antwort auf One-to-Many-Beziehungen. Der vollständige Walkthrough steht in Single-Table Design.
3. Application-Side Join (zwei Reads, in Code stitchen)
Lies aus Tabelle A, nimm die zurückgegebenen Keys, lies aus Tabelle B und führe die beiden Result-Sets in deiner Anwendung zusammen. Das ist die relationale Join-Logik — sie läuft nur in deinem Code statt in der Datenbank:
// "Get each order with its customer name" — the manual join.
const {Items: orders} = await ddb.query({TableName: 'Orders' /* … */});
const customers = await Promise.all(
orders.map((o) => ddb.get({TableName: 'Customers', Key: {id: o.customerId}}))
);
const joined = orders.map((o, i) => ({
...o,
customerName: customers[i].Item?.name
}));Fein bei kleinem Fan-out. Bei vielen Orders wird daraus ein N+1-Problem — ein
Read, um die Orders zu listen, dann ein Read pro Order —, was langsam ist und
Read Capacity verbrennt. BatchGetItem (gleich) kollabiert diese zweite Welle in
einen Round-Trip.
4. BatchGetItem (ein Round-Trip, mehrere Tabellen)
BatchGetItem
ist das, was in der API einem „zwei Tabellen auf einmal anfassen“ am nächsten
kommt: Ein Request liefert "the attributes of one or more items from one or more
tables," bis zu 100 Items oder 16 MB pro Aufruf, je nachdem, was zuerst
erreicht wird. Das spart die Round-Trips eines App-seitigen Joins — aber es ist
kein Join. AWS beschreibt es als "identify requested items by primary key";
es gibt keine ON-Condition und kein relationales Matching. Du musst die Keys
weiterhin vorher kennen und die Responses selbst zusammenstitchen.
Wenn ein echtes JOIN unvermeidbar ist
Die vier Workarounds decken Production-Read-Pfade gut ab. Wo sie einbrechen, ist die ad-hoc, explorative, analytische Query — die, für die du nicht modelliert hast:
- „Welche Kunden in der EU haben letzten Monat eine Order über $500
aufgegeben?“ über eine
Orders-Tabelle und eineCustomers-Tabelle. - Ein einmaliger Data-Quality-Check, der zwei Entity-Typen joint.
- Reporting und Aggregate (
GROUP BY,SUM,COUNT) — wofür DynamoDB überhaupt keinen Operator hat.
Genau diese Queries kannst du nicht in eine Partition vorbacken, 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 ebenso wenig.
Die übliche Schwergewichts-Antwort lautet
nach S3 exportieren und mit Athena abfragen
(oder Athenas Federated-Query-Connector nutzen, um eine Live-Tabelle zu JOINen)
oder in ein Warehouse pipen. Für echte Analytics at Scale ist das korrekt, aber
es ist eine Menge 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
echtes SQL über deinen DynamoDB-Tabellen ausführt — inklusive JOIN, GROUP BY
und Aggregatfunktionen. Er liest die Items über die normale DynamoDB-API und
führt die relationalen Teile der Query dann im Client aus. Du kannst also
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 ein Result-Set, gegen Tabellen ohne definierte Beziehung und eine
Query-Engine ohne JOIN-Keyword.
Der ehrliche Vorbehalt — „innerhalb von DynamoDBs Access-Pattern-Regeln“: Der
Workbench liest weiterhin durch DynamoDB, ein ungebundener Join ist also ein
ungebundener Read. Die schnellsten Queries sind die, bei denen die
WHERE-Klausel (oder das ON-Attribut des Joins) auf mindestens einer Seite
einen Partition Key oder einen GSI trifft, damit
DynamoDB eine Query statt eines vollen
Scans fährt, bevor der Join läuft. Der Workbench
hebt die Constraints aus diesem Guide nicht auf — er lässt dich nur die
SQL-Frage stellen, statt das Stitching von Hand zu schreiben, und sagt dir, was
er darunter tut.
Unter GUI-Clients ist es das einzige „ja, du kannst joinen“, das tatsächlich stimmt: PartiQL und AWS' eigene NoSQL Workbench — deren Operation Builder Single-Table-Operationen und PartiQL-Statements ausführt (kein JOIN, kein Multi-Table-SELECT) — stoppen beide an der Single-Table-Wand, wie die meisten anderen GUI-Clients auch. Wie DynoTable als DynamoDB GUI abschneidet, liest du hier.
FAQ
Unterstützt PartiQL JOIN?
Nein. PartiQLs SELECT liest eine einzige Tabelle (oder einen ihrer Indexes).
Eine Multi-Table-Query liefert
ValidationException: Only select from a single table or index is supported.
Dieselbe Wand wie im Rest der API.
Kann man zwei DynamoDB-Tabellen in einer Query joinen?
Nicht nativ. Die DynamoDB-API hat kein Statement, das zwei Tabellen liest und sie
über einen Key matcht. BatchGetItem kann Items aus mehreren Tabellen in einem
Request lesen, hat aber keine ON-Condition — es gibt 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 joinen?
Nein — ein Global Secondary Index ist keine separate
Tabelle, an die du joinst; er ist eine alternative Key-Sicht auf dieselben Items.
Du fragst in einem gegebenen SELECT entweder die Tabelle oder den Index per
Query ab, nicht beide gejoint. Ein GSI lässt dich Items über einen anderen Key
erreichen, was den Bedarf für einen Join oft von vornherein erledigt.
Kann man über zwei AWS-Accounts hinweg joinen (oder zwei Tabellen in
verschiedenen Accounts)?
Nicht nativ — es gibt kein Cross-Account-Join-Primitiv. BatchGetItem kann die
Tabelle eines anderen Accounts erreichen, wenn deren Resource-based Policy deinem
Caller Zugriff gewährt (seit März 2024), hat aber weiterhin keine ON-Condition
— es bleibt also ein Multi-Table-Read, kein Join. Du liest jede Seite und joinst
die Ergebnisse in deiner Anwendung oder in einem Tool wie DynoTables Workbench.
Ist Denormalisierung wirklich besser als ein Join? Für DynamoDBs Ziel-Workload — vorhersagbare, hochvolumige Reads — ja. Du verschiebst die Kosten auf die Write-Zeit (und nimmst etwas Datenduplikation in Kauf) im Tausch gegen Single-Request-Reads, die flach skalieren. Der Guide Single-Table Design behandelt die Trade-offs.
Die Keys und Conditions für diese Reads von Hand zu bauen ist fummelig — der
Expression Builder generiert dir die
KeyConditionExpression- / FilterExpression-Syntax, und
DynoTable fährt das echte SQL, wenn ein Workaround nicht reicht.
PartiQL-Rejection-Messages reproduziert am 2026-08-11 gegen DynamoDB Local (us-east-1,
@aws-sdk/client-dynamodb v3.1096.0), wörtlich aus ValidationException.message zitiert.