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/sket 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_withest la récompense. Un préfixe de type sur la clé de tri permet à un seulQueryde récupérer une entité entière, ou juste une tranche, sansScanni filtre.- Le coût : la lisibilité. Un vidage brut de
pk/skne 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 :
| pk | sk | attributes |
|---|---|---|
| TENANT#acme | META | name="Acme Inc", plan="team" |
| TENANT#acme | USER#u_3001 | email, role="admin" |
| TENANT#acme | USER#u_3002 | email, role="member" |
| TENANT#acme | INVOICE#2026-0014 | amount_cents, status="paid" |
| TENANT#acme | INVOICE#2026-0015 | amount_cents, status="open" |
| TENANT#acme | EVENT#2026-06-23T09:12Z | actor="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.
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.
| pk | sk | gsi1pk | gsi1sk |
|---|---|---|---|
| TENANT#acme | INVOICE#2026-0015 | STATUS#open | 2026-06-30 |
| TENANT#acme | INVOICE#2026-0014 | STATUS#paid | 2026-06-12 |
| TENANT#beta | INVOICE#2026-0099 | STATUS#open | 2026-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.

Pièges
- Choisis les délimiteurs une fois et ne les change jamais.
#est la convention. Mélanger#et:entre les entités cassebegins_withd'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-0015se 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 exempleUSER#etUSERGROUP#) entreront en collision sousbegins_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 tri | Exemple SK | Tranche de requête |
|---|---|---|---|
| Meta tenant | META | META | Get d'un seul item |
| User | USER# | USER#u_3001 | begins_with(sk, "USER#") |
| Invoice | INVOICE# | INVOICE#2026-0015 | begins_with(sk, "INVOICE#") |
| Event | EVENT# | EVENT#2026-06-23T09:12Z | queue 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.


