La dénormalisation dans DynamoDB
En venant de SQL, la dénormalisation ressemble à un péché — données dupliquées, aucune source unique de vérité. Dans DynamoDB c'est tout l'intérêt. Il n'y a pas de jointures, donc tu copies les données liées sur l'item qui en a besoin et tu les relis d'un seul coup.
Qu'est-ce que la dénormalisation dans DynamoDB ?
La dénormalisation dans DynamoDB consiste à copier les données liées sur l'item qui les lit, pour qu'une seule requête renvoie tout d'un coup. Comme DynamoDB n'a pas de jointures, tu pré-joins à l'écriture au lieu de coudre des tables ensemble à la lecture. Le compromis, c'est l'obsolescence — ne duplique que les valeurs qui changent rarement.
- Pas de jointures signifie que tu pré-joins à l'écriture. Stocke la valeur liée sur l'item qui la lit, pour qu'une requête n'ait jamais besoin d'un second lookup.
- Deux saveurs. Embarque des données imbriquées dans un attribut complexe sur un item, ou duplique une valeur sur plusieurs items.
- Le footgun est l'obsolescence. Quand la source change, chaque copie est fausse jusqu'à ce que tu propages la mise à jour. Ne duplique que les valeurs qui changent rarement.
- Ça achète des lectures, pas des écritures. Tu échanges des écritures plus nombreuses (et plus soignées) contre des lectures bon marché en une seule requête.
Pourquoi il n'y a pas de jointures sur lesquelles se rabattre
Un JOIN relationnel réassemble des lignes normalisées à la lecture. DynamoDB n'a pas
de jointure — un Query lit une seule collection d'items et rend exactement ce qui y
est stocké. Rien ne couture deux tables ensemble pour toi.
Donc les données doivent déjà être façonnées pour la lecture. Si un écran a besoin d'un post et du nom de son auteur, ce nom doit vivre quelque part que la lecture du post touche déjà. Le papier Amazon Dynamo de 2007 a rendu ce compromis explicite : abandonner les fonctionnalités relationnelles pour obtenir des lectures prévisibles, en quelques millisecondes à un chiffre, à l'échelle.
Pattern 1 — embarquer avec un attribut complexe
Les attributs DynamoDB peuvent contenir des maps et des lists imbriquées, pas seulement des scalaires. Donc une forme courante de dénormalisation consiste à fourrer un objet enfant directement dans son item parent au lieu de lui donner son propre item.
Un post avec ses tags et un petit snapshot d'auteur, le tout sur un seul item :
| PK | SK | author | tags |
|---|---|---|---|
| POST#9f3 | META | {id: U#12, name: "Mara Vance"} | ["dynamodb","aws"] |
Un seul GetItem renvoie le post, les tags et le bloc auteur ensemble. Pas de seconde
lecture. C'est idéal pour des données possédées par le parent et bornées en taille
— une poignée de tags, un snapshot d'auteur.
La limite à respecter : un seul item DynamoDB plafonne à 400 Ko, noms et valeurs d'attributs inclus (Service Quotas). Embarque une liste non bornée (chaque commentaire d'un post viral) et tu le dépasseras.
Pattern 2 — dupliquer une valeur sur plusieurs items
Le cas du blog est l'exemple type. Tu listes des posts et tu veux que chaque ligne affiche le nom d'affichage de l'auteur — mais tu ne veux pas d'une seconde lecture par post pour le récupérer.
Donc tu écris le nom de l'auteur sur chaque item de post à la création du post :
| PK | SK | authorId | authorName | title |
|---|---|---|---|---|
| POST#9f3 | META | U#12 | "Mara Vance" | "Modeling 1:N" |
| POST#a71 | META | U#12 | "Mara Vance" | "Sparse GSIs" |
| POST#b04 | META | U#88 | "Lio Tan" | "Query vs Scan" |
Maintenant Query PK begins_with "POST#" (ou un GSI sur les posts) rend toute la
liste — titre et auteur — sans lookup par ligne. Le nom de l'auteur est
dénormalisé : la copie canonique vit sur USER#12, et chaque post porte sa propre
copie.
Le compromis est juste là. Tu as transformé une lecture N+1 en une seule lecture, au
prix de détenir "Mara Vance" en N+1 endroits.
Embarquer vs. dupliquer — lequel
| Embarquer (attribut complexe) | Dupliquer (copie sur les items) | |
|---|---|---|
| Forme | enfant imbriqué dans le parent | même valeur sur plusieurs items |
| Idéal pour | données bornées, possédées par le parent | valeur partagée que plusieurs items affichent |
| Lecture | un GetItem | un Query |
| Coût de mise à jour | réécrire l'unique item parent | propager à chaque copie |
| Risque de taille | plafond d'item de 400 Ko | aucun par item |
Opte pour embarquer quand l'enfant n'apparaît jamais sans son parent. Opte pour dupliquer quand plusieurs items indépendants ont besoin d'afficher la même valeur partagée.
Le footgun : les copies périmées
Voici la partie qui mord. Mara se renomme « Mara V. ». Tu mets à jour USER#12.
Chaque item de post dit encore "Mara Vance" jusqu'à ce que tu ailles les corriger.
Donc mettre à jour une valeur dupliquée est une écriture en fan-out, pas un one-liner. Tu requêtes chaque item concerné et tu réécris chacun — idéalement gardé pour ne toucher que les lignes qui détiennent encore l'ancienne valeur :
UPDATE POST#9f3
SET authorName = "Mara V."
WHERE authorName = "Mara Vance"
Tu peux composer ce SET conditionnel sur authorName dans l'
Expression Builder et copier l'UpdateExpression
et la ConditionExpression générés directement dans ton code.
Le fan-out lui-même est une écriture par item : requête les posts de l'auteur, puis émets les mises à jour. La séquence :
Le coût de la duplication des données : chaque changement de la source est une requête plus une écriture par copie.
C'est pourquoi la règle est ne duplique que les valeurs qui changent rarement. Un nom d'affichage, un palier de plan, un libellé de catégorie — très bien. Un compteur en direct ou un champ fréquemment édité — non ; le fan-out te dévorera vivant.
Quand la normalisation l'emporte encore
Si une valeur change souvent, ou qu'un item est lu selon des modes véritablement imprévisibles, garde-la normalisée et accepte la lecture en plus. La dénormalisation est une optimisation pour des modes d'accès connus, à lecture intensive — pas un défaut à appliquer partout. Pré-joins les lectures que tu exécutes vraiment, et laisse le reste tranquille.
Pour décider où ces attributs dupliqués vivent, modélise d'abord les modes d'accès — voir single-table design et, pour le côté lecture du compromis, Query vs Scan.
Télécharge DynoTable pour inspecter une table dénormalisée, repérer quelles copies ont dérivé, et exécuter la mise à jour en fan-out sur tes propres données.