Relations plusieurs-à-plusieurs dans DynamoDB
Un étudiant s'inscrit à plusieurs cours ; un cours accueille plusieurs étudiants. En SQL, tu te
tournes vers une table de jointure et un JOIN à trois tables.
DynamoDB n'a pas de jointures, la relation doit donc vivre dans les clés — et l'astuce est de
stocker chaque arête d'inscription sous une forme que les deux côtés peuvent Query directement.
Ce guide déroule le problème étudiants ↔ cours de bout en bout : les modes d'accès, le pattern de qui les résout, un schéma de clés original que tu peux copier, et comment relire les deux directions sans jamais scanner la table.
Comment modéliser une relation plusieurs-à-plusieurs dans DynamoDB ?
DynamoDB n'a pas de jointures, tu modélises donc une relation plusieurs-à-plusieurs avec le pattern de : stocke chaque lien comme son propre item d'arête indexé par un côté, puis ajoute un GSI inversé qui échange les clés. Une seule arête, écrite une fois, répond ensuite à bas coût aux requêtes des deux directions.
- Stocke chaque inscription comme son propre item d'arête, pas comme un attribut liste sur l'un ou l'autre côté.
- Indexe l'arête par l'étudiant (
PK = STU#…,SK = ENROLL#CRS#…) pour qu'un seulQueryrenvoie toute la liste de cours d'un étudiant. - Ajoute un inversé qui échange les rôles (
GSI1PK = CRS#…) pour que la même arête réponde aussi à « qui est dans ce cours ? ». - Une arête, écrite une fois, se lit à bas coût dans les deux sens — c'est tout le jeu.
Formule d'abord les modes d'accès
La modélisation DynamoDB part des modes d'accès : tu décides des lectures avant de choisir le moindre nom d'attribut. Une relation plusieurs-à-plusieurs a presque toujours deux lectures symétriques en plus des recherches d'entités :
- Obtenir le profil d'un étudiant, et lister chaque cours auquel cet étudiant est inscrit.
- Obtenir les métadonnées d'un cours, et lister chaque étudiant inscrit à ce cours.
- Rechercher une seule arête d'inscription — pour mettre à jour une note ou abandonner le cours.
La difficulté : les deux lectures de liste pointent dans des directions opposées à travers le même
ensemble d'arêtes. Une conception naïve en sert une à bas coût et force un Scan pour l'autre — le
piège exact couvert dans Query vs Scan.
Le travail consiste à faire des deux directions un seul Query.
Utilise le pattern de liste d'adjacence
La recommandation propre à DynamoDB pour les relations est la liste d'adjacence : modélise chaque relation comme un item dont la clé de partition est un point d'extrémité et la clé de tri l'autre.
AWS documente cela sur la page Best Practices for Managing Many-to-Many Relationships du Guide du développeur DynamoDB.
Pourquoi des clés et pas une seconde table ? Parce que la primitive que DynamoDB te donne est un
Query contre une seule partition.
Un Query lit une plage contiguë de valeurs de clé de tri sous une seule clé de partition en une
seule opération facturée — c'est la seule « jointure » que le moteur propose.
Pour obtenir une relation qui se lit à bas coût depuis les deux côtés, tu dupliques l'arête : écris-la une fois indexée par l'étudiant, puis utilise un index secondaire pour projeter la même arête indexée par le cours.
C'est le raisonnement des clés surchargées de la conception à table unique, appliqué à une relation au lieu d'une hiérarchie parent-enfant.
La forme est deux vues empilées de la même arête — la table de base indexée par l'étudiant, le GSI inversé indexé par le cours :
Chaque arête est écrite une fois sur la table de base et projetée dans le GSI avec ses clés
échangées, si bien qu'un Query contre l'une ou l'autre partition lit la relation à bas coût.
La filiation remonte à l'article Amazon Dynamo de 2007 : la clé de partition est l'unité de distribution, et l'accès par clé unique est le chemin rapide.
Les relations dans DynamoDB sont un exercice consistant à plier des lectures plusieurs-à-plusieurs dans ce chemin rapide.
Travaille l'exemple : étudiants ↔ cours
Utilise une seule table avec des clés génériques, PK et SK, et encode le type d'entité dans la
valeur. L'arête d'inscription en est le cœur :
| PK | SK | attributes |
|---|---|---|
| STU#a91 | PROFILE | name, year, major |
| STU#a91 | ENROLL#CRS#math204 enrolledOn, grade | |
| STU#a91 | ENROLL#CRS#cs101 | enrolledOn, grade |
| CRS#math204 | METADATA | title, credits, term |
| CRS#cs101 | METADATA | title, credits, term |
Un seul Query PK = "STU#a91" renvoie le profil de l'étudiant et chaque inscription en une seule
lecture. Restreins-le avec SK begins_with "ENROLL#" pour n'obtenir que les arêtes de cours. Cela
résout « lister les cours d'un étudiant ».
Mais « lister les étudiants d'un cours » pointe dans l'autre sens — et la table de base ne peut pas y répondre, car l'id de l'étudiant est dans la clé de partition, pas dans la clé de tri.
Ajoute un index secondaire global inversé qui échange les rôles. Donne aux items d'arête une paire
générique GSI1PK/GSI1SK portant le cours du côté partition et l'étudiant du côté tri :
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| STU#a91 | ENROLL#CRS#math204 | CRS#math204 | STU#a91 |
| STU#b30 | ENROLL#CRS#math204 | CRS#math204 | STU#b30 |
| STU#a91 | ENROLL#CRS#cs101 | CRS#cs101 | STU#a91 |
Maintenant, Query GSI1 WHERE GSI1PK = "CRS#math204" liste chaque étudiant de ce cours — la lecture
que la table de base ne pouvait pas servir. Un seul item d'arête, écrit une fois, répond aux deux
directions.
Ce doit être un GSI, pas un LSI : la partition du cours est entièrement différente de la partition de l'étudiant, et un LSI partage la clé de partition de la table de base.
L'index s'étend sur plusieurs partitions, il doit donc être global — voir GSI vs LSI.
Un piège : les GSI dans DynamoDB sont renseignés de façon asynchrone. Une inscription toute neuve peut
mettre un instant à apparaître dans la direction CRS#….
Traite la lecture de la liste des inscrits comme — ce que le Guide du développeur signale explicitement pour les index secondaires globaux.
Écris et lis dans DynoTable
Écrire l'inscription revient à renseigner quatre attributs de clé plus les données propres à l'arête.
La condition qui empêche un étudiant de s'inscrire deux fois au même cours est une garde
attribute_not_exists(PK) sur la clé composite.
C'est exactement le genre de condition que tu peux assembler visuellement avec le
Générateur d'expressions DynamoDB au lieu d'écrire à la main les
ExpressionAttributeNames et les valeurs de placeholder.
Dans DynoTable, tu pointes un Query sur GSI1, tu règles GSI1PK = "CRS#math204", et la liste des
inscrits revient sous forme de table que tu peux lire, trier et éditer sur place — les deux directions
de la relation parcourables depuis un seul schéma.

Pièges et étapes suivantes
- Ne stocke pas un côté comme un attribut liste. Un tableau
courseIdssur l'item étudiant semble bien rangé jusqu'à ce qu'un cours ait besoin de sa liste d'inscrits, que le tableau atteigne le plafond de 400 Ko par item, ou que deux inscriptions entrent en compétition et s'écrasent l'une l'autre. Des items d'arête distincts passent à l'échelle et se mettent à jour indépendamment. - Garde les données d'arête sur l'arête. La
gradeet laenrolledOnde l'inscription appartiennent à l'item d'arête, pas dupliquées sur l'étudiant ou le cours — il y a exactement une ligne par paire (étudiant, cours) à mettre à jour. - Fais attention à la propagation du GSI. La direction de l'index inversé est à cohérence à terme, si bien qu'une lecture immédiatement après une inscription peut accuser un retard d'une fraction de seconde.
- Ne projette que ce dont la liste des inscrits a besoin. Une projection
KEYS_ONLYou étroite garde le GSI petit quand la vue de la liste n'a besoin que des ids.
Pour aller plus loin sur les patterns environnants, lis conception à table unique pour les clés surchargées et GSI vs LSI pour savoir quand l'index inversé doit être global. Et pour partir de tes propres relations, l'outil de Single-Table Design gratuit transforme une liste de modes d'accès du type « lister les cours d'un étudiant / lister les étudiants d'un cours » en plan PK/SK/GSI avec des items d'exemple.
Puis télécharge DynoTable pour modéliser le schéma étudiants ↔ cours pour de vrai —
écris les arêtes, construis la condition avec le Générateur d'expressions, et interroge les deux
directions de la relation sans un seul scan. Et quand tu veux quand même la vue
JOIN classique à trois tables, le SQL Workbench de
DynoTable l'exécute sur tes tables en direct.


