Avancé8 min de lecture

La surcharge de clés dans DynamoDB

Quand tu viens du SQL, une colonne signifie une seule chose pour toujours : orders.created_at est toujours une date, users.email est toujours un e-mail. La surcharge de clés balaie tout ça. Tu donnes à la clé de partition et à la des noms génériques — pk, sk — et tu laisses chaque type d'item y verser un sens différent. Une table, plusieurs entités, une seule forme.

Qu'est-ce que la surcharge de clés dans DynamoDB ?

La surcharge de clés consiste à stocker de nombreux types d'entités dans une seule table sous des noms de clés génériques comme pk/sk, en encodant le type dans la valeur (USER#u_3001, INVOICE#2026-0014). Le nom de l'attribut reste neutre pour que les utilisateurs, les factures et les événements partagent une même partition ; la valeur porte le type, et un préfixe de clé de tri permet à un seul Query de découper chaque entité via begins_with.

  • Noms de clés génériques, valeurs typées. Nomme tes clés pk/sk et mets le type d'entité dans la valeur : pk = "TENANT#acme", sk = "USER#u_3001". Le nom est bête ; c'est la valeur qui porte le type.
  • C'est ce qui fait fonctionner le single-table design. Sans surcharge, une table partagée n'est qu'un fourre-tout. Avec elle, chaque entité se trouve dans une partition que tu peux Query.
  • begins_with est la récompense. Un préfixe de type sur la clé de tri permet à un seul Query de récupérer une entité entière, ou juste une tranche, sans Scan ni filtre.
  • Le coût : la lisibilité. Un vidage brut de pk/sk ne t'apprend rien. Il te faut un visualiseur qui décode les préfixes, sinon tu vas plisser les yeux sur des chaînes.

Pourquoi les noms génériques battent les vrais noms

DynamoDB t'offre au plus deux attributs de clé par table, et un Query ne peut cibler qu'une seule clé de partition. Donc si tu nommes ta clé userId, seuls les items utilisateur peuvent vivre proprement dans cette table — tout le reste doit simuler un userId ou migrer vers sa propre table.

La surcharge contourne cela. Un nom neutre comme pk ne s'engage sur aucune entité, donc un utilisateur, une facture et un événement d'audit peuvent tous partager le même attribut de clé et la même table. C'est la valeur, et non le nom de l'attribut, qui dit ce qu'est l'item.

C'est le geste qui transforme le single-table design de la théorie en quelque chose que tu peux réellement interroger. La table partagée est le conteneur ; la surcharge est ce qui permet à des entités distinctes d'y coexister.

Un exemple multi-locataire

Disons que tu gères un produit de facturation SaaS. Chaque locataire a des membres, des factures et une piste d'audit. Au lieu de trois tables, mets tout dans une seule et surcharge les clés :

pkskattributes
TENANT#acmeMETAname="Acme Inc", plan="team"
TENANT#acmeUSER#u_3001email, role="admin"
TENANT#acmeUSER#u_3002email, role="member"
TENANT#acmeINVOICE#2026-0014amount_cents, status="paid"
TENANT#acmeINVOICE#2026-0015amount_cents, status="open"
TENANT#acmeEVENT#2026-06-23T09:12Zactor="u_3001", action="invite"

Chaque ligne partage pk = "TENANT#acme", elles forment donc une seule — toutes co-localisées, toutes accessibles en une seule lecture de partition.

Partition : TENANT#acmesk: METAsk: USER#u_3001sk: INVOICE#2026-0015sk: EVENT#2026-06-23T09:12ZUn seul Query

Le préfixe de clé de tri fait le vrai travail. Il regroupe les entités et les ordonne.

Interroger la collection surchargée

Comme le type vit dans le préfixe de la clé de tri, begins_with découpe la partition par entité sans rien scanner :

Query pk = "TENANT#acme"  -- the entire tenant, every type
Query pk = "TENANT#acme" AND begins_with(sk, "USER#")  -- just members
Query pk = "TENANT#acme" AND begins_with(sk, "INVOICE#")  -- just invoices

Tu ne paies que pour les items que la condition trouve, pas pour toute la partition — l'inverse d'un Scan filtré, où tu paies pour lire des lignes que tu jettes ensuite. AWS appelle cela une condition de clé ; elle s'exécute sur les clés avant que la moindre donnée ne quitte la partition.

Si tu construis cette condition begins_with à la main, soigne les balises de type — un USERS# de travers au lieu de USER# ne renvoie rien, en silence. Le générateur d'expressions génère la KeyConditionExpression et la table ExpressionAttributeValues pour que les préfixes correspondent à ce que tu as réellement écrit.

Surcharge aussi l'index

La même astuce s'applique à un . Donne-lui des noms de clés génériques — gsi1pk, gsi1sk — et laisse chaque entité y écrire ce dont elle a besoin. Un seul index répond alors à des modèles que la table de base ne peut pas servir.

pkskgsi1pkgsi1sk
TENANT#acmeINVOICE#2026-0015STATUS#open2026-06-30
TENANT#acmeINVOICE#2026-0014STATUS#paid2026-06-12
TENANT#betaINVOICE#2026-0099STATUS#open2026-06-25

Maintenant Query gsi1 WHERE gsi1pk = "STATUS#open" liste toutes les factures ouvertes de tous les locataires, triées par date d'échéance — une vue inter-partitions que les clés de la table de base, limitées à un locataire, ne pourraient jamais servir. Une autre entité peut réutiliser gsi1 avec son propre sens (par exemple gsi1pk = "ROLE#admin"), de sorte qu'un seul index couvre plusieurs lectures. N'oublie simplement pas qu'un GSI est en cohérence à terme — ses écritures sont en retard sur la table de base.

Fais-le dans DynoTable

Des clés surchargées brutes sont hostiles à la lecture : INVOICE#2026-0015 et EVENT#2026-06-23T09:12Z se confondent dans une liste plate. Un visualiseur qui regroupe par partition et met en avant les préfixes retransforme le fourre-tout en entités.

DynoTable parcourant la collection d'items d'un locataire — les items META, USER, INVOICE et EVENT regroupés sous une seule clé de partition surchargée.
DynoTable parcourant la collection d'items d'un locataire — les items META, USER, INVOICE et EVENT regroupés sous une seule clé de partition surchargée.

Pièges

  • Choisis les délimiteurs une fois et ne les change jamais. # est la convention. Mélanger # et : entre les entités casse begins_with d'une manière dont rien ne t'avertit.
  • Ne surcharge pas des valeurs qui ont besoin de calculs d'intervalle. Une clé de tri INVOICE#2026-0015 se trie lexicalement, pas numériquement — et utilise des dates ISO-8601 pour que l'ordre des chaînes corresponde à l'ordre voulu.
  • Réserve l'espace de noms des préfixes. Deux types d'entité qui commencent tous deux par USER (par exemple USER# et USERGROUP#) entreront en collision sous begins_with(sk, "USER"). Rends les préfixes non ambigus dès le premier caractère.
  • Planifie la lecture avant les clés. La surcharge sert des modes d'accès que tu as énumérés. Si tu ne connais pas encore tes lectures, consulte d'abord le single-table design — les clés découlent des requêtes.

Esquisse une partition, puis télécharge DynoTable pour parcourir tes propres clés surchargées et regarder un seul Query ramener un locataire entier d'un coup.

Coût de requête sur une partition surchargée

Lister chaque membre sous TENANT#acme avec begins_with(sk, "USER#") ne lit que les lignes utilisateur — pas les factures ni les événements — parce que la condition de clé filtre avant que les données ne quittent la partition. Sur un tenant avec 200 utilisateurs (2 Ko chacun) et 5 000 événements d'audit (1 Ko chacun), cette requête touche ~400 Ko (~100 RCU à cohérence à terme). Un Scan sur toute la table pour trouver les utilisateurs facturerait chaque item de chaque tenant.

Colle des items surchargés représentatifs dans le calculateur de taille d'item, puis estime les requêtes de liste dans le calculateur de tarifs.

Concevoir avec l'outil single-table

Entre les entités (Tenant, User, Invoice, Event) et les modes d'accès (« lister les utilisateurs d'un tenant », « factures ouvertes cross-tenants ») dans l' outil de single-table design. Il propose des templates pk/sk et des clés GSI qui matchent les préfixes de surcharge que tu utiliseras en production — avant de committer du CloudFormation.

Émettre des requêtes depuis les patterns

Une fois les préfixes fixés, construis les conditions de clé dans l' expression builder et exporte un programme paginé depuis le query builder. Les typos de préfixe (USER# vs USERS#) renvoient des ensembles vides sans erreur — les expressions générées réduisent ce mode d'échec silencieux.

Registre de préfixes de type d'entité

Maintiens une courte table interne que les développeurs peuvent consulter :

EntitéPréfixe de triExemple SKTranche de requête
Meta tenantMETAMETAGet d'un seul item
UserUSER#USER#u_3001begins_with(sk, "USER#")
InvoiceINVOICE#INVOICE#2026-0015begins_with(sk, "INVOICE#")
EventEVENT#EVENT#2026-06-23T09:12Zqueue ordonnée dans le temps avec lecture décroissante

Les nouveaux types d'entité doivent choisir des préfixes qui ne collisionnent pas sous begins_with des préfixes existants — USER# et USERGROUP# matchent tous deux begins_with(sk, "USER") sauf si tu allonges ou sépares soigneusement par délimiteur.

Mis à jour