La dénormalisation dans DynamoDB
En venant de SQL, la dénormalisation sonne comme un péché — données dupliquées, pas de source unique de vérité. Dans DynamoDB, c'est tout l'objectif. 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 seul coup. Comme DynamoDB n'a pas de jointures, tu pré-joins au moment de l'écriture au lieu d'assembler des tables au moment de la lecture. Le compromis est l'obsolescence — ne duplique que les valeurs qui changent rarement.
- Pas de jointures signifie que tu pré-joins au moment de l'écriture. Stocke la valeur liée sur l'item qui la lit, pour qu'une requête n'ait jamais besoin d'une seconde recherche.
- Deux variantes. Imbrique des données imbriquées dans un attribut complexe sur un item, ou duplique une valeur sur de nombreux items.
- Le piège est l'obsolescence. Quand la source change, chaque copie est fausse jusqu'à ce que tu diffuses la mise à jour. Ne duplique que les valeurs qui changent rarement.
- Cela 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 au moment de la lecture. DynamoDB n'a pas de
jointure — une Query lit une et renvoie exactement ce qui y est
stocké. Rien n'assemble deux tables pour toi. (Sur le chemin de lecture en production,
du moins — pour l'audit ad hoc ou la vérification de dérive, le SQL Workbench de
DynoTable exécute un vrai JOIN sur DynamoDB côté client.)
Donc les données doivent déjà être façonnées pour la lecture. Si un écran a besoin d'un article et du nom de son auteur, ce nom doit se trouver quelque part que la lecture de l'article touche déjà. L'article Amazon Dynamo de 2007 a rendu ce compromis explicite : abandonner les fonctionnalités relationnelles pour obtenir des lectures prévisibles à grande échelle — le compromis que DynamoDB délivre désormais sous forme de lectures en quelques millisecondes à un chiffre.
Motif 1 — imbriquer avec un attribut complexe
Les attributs DynamoDB peuvent contenir des maps et des listes imbriquées, pas seulement des scalaires. Donc une forme courante de dénormalisation consiste à fourrer un objet enfant directement à l'intérieur de son item parent au lieu de lui donner son propre item.
Un article avec ses tags et un petit instantané d'auteur, tout sur un seul item :
| PK | SK | author | tags |
|---|---|---|---|
| POST#9f3 | META | {id: U#12, name: "Mara Vance"} | ["dynamodb","aws"] |
Un seul GetItem renvoie l'article, les tags et le bloc auteur ensemble. Pas de
seconde lecture. C'est excellent pour des données possédées par le parent et bornées en
taille — une poignée de tags, un instantané d'auteur.
La limite à respecter : un seul item DynamoDB plafonne à 400 Ko, noms d'attribut et valeurs inclus (Service Quotas). Imbrique une liste non bornée (tous les commentaires d'un article viral) et tu la dépasseras.
Motif 2 — dupliquer une valeur sur plusieurs items
Le cas du blog est l'exemple canonique. Tu listes des articles et tu veux que chaque ligne affiche le nom d'affichage de l'auteur — mais tu ne veux pas d'une seconde lecture par article pour l'obtenir.
Alors tu écris le nom de l'auteur sur chaque item d'article quand l'article est créé :
| 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" |
Un sur les articles (disons GSI1PK = "POST", ou un index clé sur l'auteur) rend toute la
liste — titre et auteur — sans recherche par ligne. begins_with sur la clé de partition n'existe
pas ; une Query a besoin d'une égalité de clé de partition, donc avec des clés de partition par article la liste vient
du GSI, et non d'une Query sur POST#. Le nom de l'auteur est dénormalisé :
la copie canonique vit sur USER#12, et chaque article porte sa propre copie.
Le compromis est là même. Tu as transformé une lecture N+1 en une seule lecture, au prix de
détenir "Mara Vance" en N+1 endroits.
Imbriquer vs. dupliquer — lequel
| Imbriquer (attribut complexe) | Dupliquer (copie sur plusieurs 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 | une valeur partagée affichée par plusieurs items |
| Lecture | un GetItem | une Query |
| Coût de mise à jour | réécrire le seul item parent | diffuser sur chaque copie |
| Risque de taille | plafond d'item de 400 Ko | aucun par item |
Opte pour l'imbrication quand l'enfant n'apparaît jamais qu'avec son parent. Opte pour la duplication quand de nombreux items indépendants ont besoin d'afficher la même valeur partagée.
Le piège : les copies obsolètes
Voici la partie qui mord. Mara se renomme « Mara V. ». Tu mets à jour
USER#12. Chaque item d'article dit encore "Mara Vance" jusqu'à ce que tu ailles les corriger.
Donc mettre à jour une valeur dupliquée est une écriture en diffusion, pas un simple appel. Tu interroges chaque item concerné et 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 le
Générateur d'expressions et copier l'UpdateExpression
et la ConditionExpression générées directement dans ton code.
La diffusion elle-même est une écriture par item : interroge le GSI clé sur l'auteur pour les articles de cet 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. Dans DynoTable, la diffusion atterrit d'abord dans la zone de staging, sous forme de diff par attribut et par item vérifiable — tu vois chaque copie sur le point de changer avant que quoi que ce soit ne parte.
C'est pourquoi la règle est ne dupliquer que les valeurs qui changent rarement. Un nom d'affichage, un palier de forfait, une étiquette de catégorie — d'accord. Un compteur en direct ou un champ fréquemment modifié — non ; la diffusion te dévorera vivant.
Coût d'écriture du fan-out
Chaque copie que tu mets à jour est une écriture facturée à part. Dans
us-east-1 en on-demand, mettre à jour 50 items de posts après le
renommage d'un auteur coûte 50 × WCU — typiquement 1 WCU par Ko et par
item quand chaque ligne de post fait ≤ 1 Ko. Le côté lecture reste une seule
Query ; le côté écriture croît avec le nombre de doublons que tu entretiens.
Estime les deux chemins dans le
calculateur de tarifs.
Quand la normalisation gagne encore
Si une valeur change souvent, ou si un item est lu selon des motifs vraiment imprévisibles, garde-la normalisée et accepte la lecture supplémentaire. La dénormalisation est une optimisation pour des motifs d'accès connus et à forte lecture — pas un défaut à appliquer partout. Pré-joins les lectures que tu exécutes réellement, et laisse le reste tranquille.
Pour décider où ces attributs dupliqués vivent, modélise d'abord les motifs d'accès — voir la conception à table unique 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 diffusion sur tes propres données. Son
SQL Workbench peut même JOIN la source de vérité avec les
copies pour trouver la dérive en une seule requête.