Les index creux DynamoDB
Un index creux est un index secondaire qui ne contient que les items portant son attribut de clé — de sorte qu'un petit sous-ensemble actif d'une table énorme devienne sa propre collection préfiltrée, prête à requêter.
Tu as des millions de lignes mais la requête que tu exécutes toute la journée ne touche qu'une infime tranche : les tickets de support ouverts, les factures impayées, les comptes signalés pour révision.
Filtrer cette tranche scanne quand même toute la table et te facture chaque lecture. Un index creux rend l'index lui-même petit à la place.
Qu'est-ce qu'un index creux dans DynamoDB ?
Un index creux est un index secondaire qui ne contient que les items portant son attribut de clé. Parce que DynamoDB ignore tout item dépourvu de cette clé, tu inventes une clé que seuls les items voulus écrivent — tickets ouverts, factures impayées — et l'index devient exactement ce sous-ensemble. Les requêtes ne lisent alors que lui, pas de filtre, pas de capacité de lecture gaspillée.
- Un index secondaire n'indexe que les items qui ont sa clé. Omets la clé sur un item et il n'entre jamais dans l'index — pas de placeholder, pas de ligne nulle.
- Donc tu inventes une clé que seuls les items voulus portent. Écris-la sur les items que tu requêtes, retire-la sur les autres. L'index devient exactement ce sous-ensemble.
- La requête ne lit que le sous-ensemble, pas de filtre. Sa taille suit le petit ensemble actif, pas le total de la table.
REMOVEest le levier, pas le vidage. Une chaîne vide n'est pas une clé d'index valide — DynamoDB rejette toute l'écriture avec une ValidationException — donc tu dois supprimer l'attribut.
Le problème : filtrer n'économise pas de lectures
En venant de SQL, tu supposes qu'une clause WHERE restreint le travail. La
FilterExpression de DynamoDB ne le fait pas. Elle s'exécute après que les items sont lus, pas avant.
Selon le Guide du développeur AWS, « une Query consomme la même quantité de capacité de lecture, qu'une expression de filtre soit présente ou non » — tu paies pour chaque item examiné, puis tu jettes les non-correspondances.
Donc si 50 de tes 5 millions de tickets sont ouverts, une Query/Scan filtrée lit
à travers des millions pour te remettre ces 50.
C'est le piège derrière chaque fil « pourquoi mon scan est-il si coûteux » ; query vs scan donne l'image complète des coûts.
Un index creux le contourne en rendant l'index lui-même petit.
Comment fonctionne la parcimonie
Un index secondaire n'indexe que les items qui ont réellement les attributs de clé de l'index.
La doc AWS sur les index creux l'explicite : DynamoDB écrit un item dans un index secondaire uniquement quand cet item porte les attributs de clé de l'index, donc un index sur un attribut rarement défini reste naturellement petit.
Rate la clé de partition (ou la clé de tri) du GSI sur un item et DynamoDB ne l'écrit simplement pas dans l'index. Pas de placeholder, pas de ligne nulle — l'item est absent.
Cette « absence par défaut » est toute l'astuce. N'indexe pas un attribut status
que chaque item porte. Invente un attribut que seuls les items que tu veux
requêter portent tout court.
L'index devient alors une liste propre d'exactement ces items, et une Query contre
lui ne lit qu'eux — pas de filtre, pas de capacité gaspillée.
Imagine la table de base alimentant l'index, où seuls les items portant la clé passent :
Seuls les items clés (ouverts) se répliquent vers l'index ; les items fermés n'y entrent jamais.
C'est le même état d'esprit de mise en forme des clés que le single-table design : les clés sont des outils que tu construis pour un mode d'accès spécifique, pas des miroirs fidèles de tes données.
Un exemple concret : « les tickets ouverts uniquement »
Prends une table de tickets de support. La table de base est clée pour récupérer un ticket par id et lister les tickets d'un client :
| PK | SK | attributes |
|---|---|---|
| TICKET#a91f | DETAIL | subject, body, priority, openState |
| CUSTOMER#88 | TICKET#a91f | subject, priority, openState |
Au cours de la vie de la table, la plupart des tickets finissent fermés. Mais la requête du tableau de bord que tes agents frappent toute la journée est « montre-moi tous les tickets ouverts, du plus ancien au plus récent » — quelques centaines de lignes cachées parmi des millions.
Le geste de l'index creux : définis un avec la clé de partition openBucket et la clé de tri
openedAt, et n'écris openBucket que sur les tickets ouverts. Définis-le quand le
ticket est créé ; fais un REMOVE quand le ticket est résolu.
| PK | SK | openBucket | openedAt | |
|---|---|---|---|---|
| TICKET#a91f | DETAIL | OPEN | 2026-06-23T09:14:00Z | ← open: in the index |
| TICKET#b02c | DETAIL | OPEN | 2026-06-22T16:40:00Z | ← open: in the index |
| TICKET#77de | DETAIL | (absent) | 2026-05-30T11:02:00Z | ← closed: NOT in the index |
Les tickets a91f et b02c portent openBucket, donc ils vivent dans le GSI. Le ticket
77de a été résolu et son openBucket retiré, donc il en est silencieusement sorti. Le
tableau de bord est maintenant une seule requête peu coûteuse :
Query IndexName = "open-tickets-index"
KeyConditionExpression: openBucket = "OPEN"
ScanIndexForward: true # oldest first
Cela ne lit que les tickets ouverts. À mesure que les tickets se ferment, l'index rétrécit tout seul — sa taille suit la population ouverte, jamais le total.
Une seule valeur de partition statique ("OPEN") convient ici précisément parce que l'ensemble reste
petit. Un ensemble ouvert énorme nécessiterait une clé de partition shardée, mais l'index « petit sous-ensemble »
est exactement là où une seule valeur est le bon choix.
La transition qui fait que ça fonctionne est une seule — retirer l'attribut quand le ticket est résolu.
Prototype cette clause REMOVE et la condition de clé typée pour le côté lecture dans le
Générateur d'expressions DynamoDB, au lieu
d'assembler à la main les ExpressionAttributeNames et les placeholders :val toi-même.
Fais-le dans DynoTable
Le plus dur avec un index creux n'est pas la lecture — c'est voir quels items sont entrés dans l'index par rapport à ceux qui en sont silencieusement sortis.
DynoTable te permet de basculer une vue de table sur un index secondaire et de voir exactement le
sous-ensemble peuplé. Ainsi tu peux confirmer qu'un ticket résolu a bien quitté
open-tickets-index au lieu de traîner avec une clé obsolète.

Pièges et étapes suivantes
Quelques points à surveiller :
- Retire la clé, ne la vide pas. Une chaîne vide n'est pas une clé d'index valide —
écrire
openBucket = ""échoue avec une ValidationException, donc l'item n'est jamais indexé avec elle. Pour retirer un item de l'index tu dois faire unREMOVEde l'attribut. - L'index est à . Les GSI se mettent à jour de façon asynchrone, donc un ticket tout juste résolu peut brièvement encore apparaître — les lectures de GSI ne prennent en charge que la cohérence à terme. Ne t'y fie pas pour « ce ticket est-il ouvert en ce moment ».
- Attention aux attributs . Une
Querysur l'index ne renvoie que les attributs projetés dedans. Si le tableau de bord a besoin du sujet et de la priorité, projette-les — ou paie unGetItemsupplémentaire pour l'item de base complet. - Les GSI comme les LSI peuvent être creux — le levier est le même : omets la clé de tri de l'index sur les items que tu ne veux pas indexer. Un GSI est généralement le meilleur choix, cependant : tu peux l'ajouter après la création de la table et lui donner son propre schéma de clé et sa propre capacité. GSI vs LSI décompose le compromis.
Les index creux sont l'une des plus anciennes idées du modèle. L'original article Amazon Dynamo de 2007 a bâti le magasin autour du service économique de modes d'accès connus et à fort volume.
Un index creux est exactement cela : mets en forme les clés pour que la requête courante ne lise rien dont elle n'a pas besoin.
Pour en construire et en inspecter un pour de vrai, télécharge DynoTable, pointe-le vers ta table, et bascule la vue de données sur ton GSI creux — regarde le sous-ensemble se mettre à jour à mesure que les items gagnent et perdent la clé d'index.


