JOIN DynamoDB : comment joindre des tables
Il n'y a pas de JOIN dans DynamoDB. L'API n'a pas d'opérateur de jointure, le modèle
de données n'a pas de clés étrangères, et — la partie qui surprend la plupart des
gens — , la couche de requête au parfum SQL, n'en ajoute pas non plus. Un
SELECT PartiQL lit exactement une table.
Si tu viens d'une base de données relationnelle, c'est le premier mur que tu heurtes. Ce guide couvre pourquoi le mur est là, les quatre choses que les développeurs font à la place, le seul cas où tu as vraiment besoin d'une vraie jointure — et comment en exécuter une.
DynamoDB peut-il faire des jointures ?
Non. DynamoDB ne peut pas joindre de tables — ni via l'API bas niveau (GetItem /
Query / Scan / BatchGetItem), ni via , ni via un quelconque
planificateur de requêtes intégré, parce qu'il n'y en a pas. Chaque lecture correspond à
une table ou à l'un de ses index ; combiner deux tables sur une clé correspondante est
quelque chose que tu fais dans ton app après que DynamoDB a renvoyé les items, jamais à
l'intérieur.
- DynamoDB n'a aucun opérateur
JOIN. Il n'en a jamais eu. - Le
SELECTde PartiQL est mono-table uniquement — la grammaire est littéralementSELECT … FROM {{table}}[.{{index}}], et le pointer vers deux tables renvoieValidationException: Only select from a single table or index is supported. - Le correctif recommandé par AWS est de ne pas avoir besoin d'une jointure : , ou utilise le single-table design pour que les items liés vivent dans une seule partition que tu récupères en une seule requête.
- Pour le vrai cas cross-table / ad hoc, tu joins en dehors de DynamoDB — dans ton app, ou avec un outil qui le fait pour toi.
Pourquoi DynamoDB n'a pas de jointures
Un JOIN SQL demande à la base de lire plusieurs tables et de les assembler au moment de la
requête. Le
guide d'AWS sur la modélisation de données relationnelles
détaille le coût : une requête comme
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 DESCest flexible, mais « chaque jointure dans la requête augmente la complexité d'exécution de la requête parce que les données de chaque table doivent être mises en scène puis assemblées ». Ce travail est non borné — son coût dépend des données, pas de la requête — ce qui est exactement la propriété que DynamoDB refuse d'avoir.
Alors AWS a intégré la contrainte par conception. DynamoDB est, selon leurs mots, « conçu
pour minimiser à la fois les contraintes [CPU et réseau] en éliminant les JOIN (et en
encourageant la dénormalisation des données) et en optimisant l'architecture de la base pour
répondre pleinement à une requête d'application avec une seule requête vers un item ». Ce
sont les qualités qui achètent une latence en millisecondes à un chiffre à n'importe quelle
échelle : le coût d'exécution d'une lecture DynamoDB est constant quelle que soit la taille
de la table. Il n'y a pas de moteur de jointure et pas de concept de clé étrangère à
planifier — par conception.
« Mais PartiQL c'est du SQL, il joint sûrement ? »
Non. PartiQL te donne la syntaxe SELECT / INSERT / UPDATE / DELETE sur DynamoDB,
mais il est compatible SQL, pas SQL. La
grammaire officielle du SELECT
est :
SELECT {{expression}} [, ...]
FROM {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]FROM prend une table (optionnellement l'un de ses index). Il n'y a pas de seconde
table FROM, pas de JOIN, pas de sous-requête, pas de CTE. Nous avons exécuté les trois
contre DynamoDB pour voir exactement comment chacun échoue.
Un JOIN explicite :
SELECT o.pk FROM "Orders" o JOIN "Customers" c ON o.customerId = c.pkValidationException: Only select from a single table or index is supported.Deux tables dans FROM — même rejet : le moteur ne refuse donc pas le mot-clé JOIN, il
refuse la seconde table :
SELECT * FROM "Orders", "Customers"ValidationException: Only select from a single table or index is supported.Une sous-requête échoue différemment, ce qui est bon à savoir si tu en débogues une.
PartiQL n'atteint même pas la vérification multi-table — il rejette l'opérande de IN avant
cela, donc tu obtiens un message qui ne mentionne jamais les tables :
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 memberLa conséquence pratique : aucune formulation ne te donne une jointure. Les deux premières meurent sur la seconde table, et la troisième meurt encore plus tôt, sur l'opérande.
Si tu veux tout le raisonnement sur pourquoi PartiQL ressemble à du SQL mais ne peut pas se comporter comme lui, voir PartiQL vs SQL.
Les 4 contournements que les devs utilisent vraiment
1. Dénormaliser (copier les données)
Stocke les champs que tu joindrais autrement directement sur l'item. Une Order porte un
instantané du customerName et de la shippingAddress au lieu d'un customerId que tu
résoudrais plus tard. Une lecture, pas de jointure.
Le coût est le fan-out au moment de l'écriture : quand la source change, tu mets à jour chaque copie (typiquement via un handler ). Tu échanges la complexité de lecture contre la complexité d'écriture — généralement un bon échange pour une app à dominante lecture.
2. Single-table design (pré-joindre dans la partition)
Mets les entités liées dans une seule table sous une clé de partition partagée pour
qu'une soit le résultat joint. Un client et toutes ses commandes
partagent PK = "CUSTOMER#42" ; un seul Query renvoie l'item client plus chaque item de
commande — la « jointure » a déjà eu lieu au moment de l'écriture.
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (the customer)
→ CUSTOMER#42 / ORDER#1001 (an order)
→ CUSTOMER#42 / ORDER#1002 (an order)
C'est la réponse canonique de DynamoDB aux relations un-à-plusieurs. Explication complète dans single-table design.
3. Jointure côté application (deux lectures, assemblage dans le code)
Lis depuis la table A, prends les clés que tu as récupérées, lis depuis la table B, et fusionne les deux jeux de résultats dans ton application. C'est la logique de jointure relationnelle — tournant juste dans ton code au lieu de la base :
// "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
}));Bien pour un petit fan-out. Avec de nombreuses commandes, ça devient un problème N+1 —
une lecture pour lister les commandes, puis une lecture par commande — ce qui est lent et
brûle de la capacité de lecture. BatchGetItem (ci-après) réduit cette seconde vague en un
seul aller-retour.
4. BatchGetItem (un aller-retour, plusieurs tables)
BatchGetItem
est ce que l'API a de plus proche de « toucher deux tables à la fois » : une requête renvoie
« les attributs d'un ou plusieurs items d'une ou plusieurs tables », jusqu'à 100 items
ou 16 MB par appel, selon ce qu'il atteint en premier. Ça coupe les allers-retours d'une
jointure côté app — mais ce n'est pas une jointure. Tu « identifies les items demandés
par clé primaire » ; il n'y a pas de condition ON ni de correspondance relationnelle. Tu
dois toujours connaître les clés à l'avance et assembler les réponses toi-même.
Quand un vrai JOIN est inévitable
Les quatre contournements couvrent bien les chemins de lecture en production. Là où ils s'effondrent, c'est la requête ad hoc, exploratoire, analytique — celle pour laquelle tu n'as pas modélisé :
- « Quels clients de l'UE ont passé une commande de plus de 500 $ le mois dernier ? » à
travers une table
Orderset une tableCustomers. - Un contrôle de qualité de données ponctuel joignant deux types d'entités.
- Reporting et agrégats (
GROUP BY,SUM,COUNT) — pour lesquels DynamoDB n'a aucun opérateur du tout.
Ce sont exactement les requêtes que tu ne peux pas pré-cuire dans une partition, parce que
par définition tu ne savais pas que tu les poserais. L'instinct relationnel — écrire un
JOIN — est le bon ici. DynamoDB ne peut simplement pas le servir nativement, et PartiQL
non plus.
La réponse habituelle et lourde est d'
exporter vers S3 et interroger avec Athena
(ou d'utiliser le connecteur de requête fédérée d'Athena pour JOIN une table live), ou de
canaliser vers un entrepôt. C'est correct pour de la vraie analytique à grande échelle, mais
c'est beaucoup de plomberie pour une question à laquelle tu veux une réponse maintenant,
contre ta table live.
Exécuter un vrai JOIN avec le SQL Workbench de DynoTable
DynoTable est un client DynamoDB de bureau dont le SQL Workbench exécute du
vrai SQL — y compris JOIN, GROUP BY et les fonctions d'agrégation — sur tes tables
DynamoDB. Il lit les items via l'API DynamoDB normale, puis exécute les parties
relationnelles de la requête dans le client. Ainsi tu peux écrire :
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— et obtenir un jeu de résultats, contre des tables qui n'ont aucune relation définie et un
moteur de requête qui n'a aucun mot-clé JOIN.
La mise en garde honnête — « dans les règles de patterns d'accès de DynamoDB » : le
Workbench lit toujours à travers DynamoDB, donc une jointure non bornée est une lecture non
bornée. Les requêtes les plus rapides sont celles où la clause WHERE (ou l'attribut ON de
la jointure) frappe une clé de partition ou un GSI sur au moins un côté,
pour que DynamoDB exécute un Query plutôt qu'un scan de table
complet avant que la jointure ne s'exécute. Le Workbench n'abroge pas les contraintes de ce
guide — il te laisse juste poser la question en SQL au lieu d'écrire l'assemblage à la
main, et il te dit ce qu'il fait en dessous.
Parmi les clients GUI, c'est le seul « oui, tu peux joindre » qui soit réellement vrai : PartiQL et le NoSQL Workbench d'AWS lui-même — dont le constructeur d'opérations exécute des opérations mono-table et des instructions PartiQL (pas de JOIN, pas de SELECT multi-table) — s'arrêtent tous deux au mur mono-table, comme la plupart des autres clients GUI. Vois comment DynoTable se compare en tant que GUI DynamoDB.
FAQ
PartiQL supporte-t-il JOIN ?
Non. Le SELECT de PartiQL lit une seule table (ou l'un de ses index). Une requête
multi-table renvoie ValidationException: Only select from a single table or index is supported. Le même mur que le reste de l'API.
Peux-tu joindre deux tables DynamoDB en une seule requête ?
Pas nativement. L'API DynamoDB n'a aucune instruction qui lit deux tables et les fait
correspondre sur une clé. BatchGetItem peut lire des items de plusieurs tables en une seule
requête, mais il n'a pas de condition ON — il renvoie les items que tu as nommés par clé
primaire et te laisse la correspondance. Un vrai JOIN … ON … n'a lieu qu'en dehors de
DynamoDB : dans ton app, ou dans le SQL Workbench de DynoTable.
Peux-tu joindre une table à son GSI ?
Non — un index secondaire global n'est pas une table séparée à laquelle
tu joins ; c'est une vue par clé alternative des mêmes items. Tu Query soit la table
soit l'index dans un SELECT donné, pas les deux joints ensemble. Un GSI te laisse
atteindre les items par une clé différente, ce qui supprime souvent le besoin d'une
jointure dès le départ.
Peux-tu joindre à travers deux comptes AWS (ou deux tables dans des comptes différents) ?
Pas nativement — il n'y a pas de primitive de jointure cross-compte. BatchGetItem peut
atteindre la table d'un autre compte si la politique basée sur les ressources de cette table
accorde l'accès à ton appelant (depuis mars 2024), mais il n'a toujours pas de condition
ON, c'est donc une lecture multi-table, pas une jointure. Tu lirais chaque côté et
joindrais les résultats dans ton application ou dans un outil comme le Workbench de DynoTable.
La dénormalisation est-elle vraiment meilleure qu'une jointure ? Pour la charge de travail cible de DynamoDB — des lectures prévisibles à haut volume — oui. Tu déplaces le coût vers le moment de l'écriture (et acceptes une certaine duplication de données) en échange de lectures en une seule requête qui scalent à plat. Le guide single-table design couvre les arbitrages.
Construire les clés et conditions de ces lectures à la main est délicat — l'
Expression Builder génère la syntaxe
KeyConditionExpression / FilterExpression pour toi, et
DynoTable exécute le vrai SQL quand un contournement ne suffit pas.
Messages de rejet PartiQL reproduits le 11 août 2026 sur DynamoDB Local (us-east-1,
@aws-sdk/client-dynamodb v3.1096.0), cités mot pour mot depuis
ValidationException.message.