Intermédiaire8 min de lecture

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 :

PKSKauthortags
POST#9f3META{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éé :

PKSKauthorIdauthorNametitle
POST#9f3METAU#12"Mara Vance""Modeling 1:N"
POST#a71METAU#12"Mara Vance""Sparse GSIs"
POST#b04METAU#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)
Formeenfant imbriqué dans le parentmême valeur sur plusieurs items
Idéal pourdonnées bornées, possédées par le parentune valeur partagée affichée par plusieurs items
Lectureun GetItemune Query
Coût de mise à jourréécrire le seul item parentdiffuser sur chaque copie
Risque de tailleplafond d'item de 400 Koaucun 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 :

"DynamoDB"App"DynamoDB"App"Mettre à jour le nom de USER"Query les articles de l'auteur""POST"Mettre à jour chaque authorName"

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 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.

Mis à jour