Avancé9 min de lecture

Comment un GSI DynamoDB est stocké en interne

Un Global Secondary Index n'est pas un pointeur de retour vers ta table. C'est une table séparée, gérée en interne — ses propres partitions, son propre schéma de clés, sa propre capacité — que DynamoDB garde synchronisée en y copiant les écritures de façon asynchrone.

En venant de SQL, un index est un B-tree boulonné sur la même table physique, mis à jour dans la même transaction. Un GSI casse ces deux hypothèses, et presque chaque surprise de GSI remonte à ce seul fait.

Comment un GSI DynamoDB est-il stocké ?

Un GSI DynamoDB est stocké comme une table séparée, gérée en interne — avec ses propres partitions, son propre schéma de clés et sa propre capacité — et non comme un pointeur vers la table de base. DynamoDB copie chaque écriture dans l'index de façon asynchrone, en ne stockant que les clés du GSI, les clés de la table de base et les éventuels attributs projetés.

  • Un GSI est sa propre table. Il a un espace de partitions totalement indépendant clé sur la clé de partition du GSI, pas celle de la table de base.
  • Les écritures se répliquent de façon asynchrone. Ton écriture est committée sur la table de base d'abord, puis DynamoDB l'éclate vers chaque GSI sur un chemin en arrière-plan.
  • Seuls les attributs projetés sont stockés. L'index contient les clés du GSI, les clés de base, plus les attributs que tu as projetés — rien d'autre.
  • La clé du GSI n'a pas besoin d'être unique. Plusieurs items de base peuvent partager une même clé de partition/tri de GSI ; la clé primaire de base est le départage qui les garde distincts.

Commence par un item de base

Prenons un journal d'audit SaaS. Chaque action privilégiée dans un espace de travail devient un événement immuable. La table de base, WorkspaceEvents, est clé pour que tous les événements d'un espace de travail vivent dans une seule collection d'items, ordonnés par le temps :

WorkspaceEvents (table de base)
EventPKEventSKactorIdverbtargetRef
WS#orbit-9TS#2026-06-23T14:02:11ZUSR#kpROLE_GRANTEDUSR#mara

EventPK = "WS#orbit-9" partitionne par espace de travail ; EventSK est un timestamp ISO pour qu'un Query renvoie les événements d'un espace de travail en ordre chronologique. Ça sert parfaitement « montre-moi la timeline de cet espace de travail ».

Ça ne sert rien d'autre. Tu ne peux pas demander « qu'a fait USR#kp à travers chaque espace de travail ? » — actorId n'est pas une clé, donc le seul moyen d'y répondre sur la table de base est un Scan complet. C'est l'access pattern qu'un GSI existe pour ajouter.

Ajoute un GSI et regarde une seconde table apparaître

Définis un GSI, ByActor, qui repartitionne les mêmes événements par qui les a réalisés :

ByActor (GSI)
GSI1PK = actorId   ("USR#kp")
GSI1SK = EventSK   ("TS#2026-06-23T14:02:11Z")

DynamoDB maintient maintenant une seconde structure physique. Le même événement logique est stocké deux fois — une fois dans la partition WS#orbit-9 de la table de base, et encore dans la partition USR#kp du GSI :

ByActor (GSI) — son propre espace de partitions
GSI1PKGSI1SKEventPKEventSKverb
USR#kpTS#2026-06-23T14:02:11ZWS#orbit-9TS#2026-06-23T14:02:11ZROLE_GRANTED

Note ce qui a fait le voyage : les clés de la table de base (EventPK, EventSK) sont stockées dans chaque item de GSI automatiquement. C'est ainsi qu'un hit de GSI peut te ramener à l'item complet — et pourquoi un index KEYS_ONLY coûte quand même du stockage.

Ce qui vit réellement dans le GSI

L'index ne copie pas l'item entier. Chaque entrée de GSI contient exactement trois choses, et tu ne contrôles que la troisième :

Stocké dans le GSID'où ça vientOptionnel ?
Clé de partition + tri du GSILes attributs que tu as nommés comme clés de GSINon
Clé(s) de la table de baseCopiées depuis chaque item de baseNon
Attributs projetésTon choix de ProjectionOui

Projection est KEYS_ONLY, INCLUDE (une liste nommée) ou ALL. Un Query sur le GSI ne peut renvoyer que les attributs présents dans l'index.

Demande-en un qui n'est pas projeté et DynamoDB ne va pas le récupérer de façon transparente — tu n'obtiens rien pour ce champ. (doc AWS sur les GSI)

C'est le piège relationnel inversé : SQL ferait une jointure de retour vers le heap pour la colonne manquante. Un GSI ne le fait jamais. La projection est tout le contrat.

Comment une écriture atteint l'index

La réplication est la partie qui casse le plus durement l'intuition SQL. Une écriture de base et sa mise à jour d'index ne sont pas une seule opération atomique.

Quand tu fais PutItem, DynamoDB committe durablement sur la table de base, acquitte ton écriture, puis propage le changement sur un chemin en arrière-plan qui met à jour chaque GSI. L'acquittement n'attend pas l'index.

Voici l'ordre des événements pour notre écriture d'audit, de haut en bas :

PutItemévénement WS#orbit-9Commit sur lapartition de base200 OKà l'appelantChemin async :extraire les clés du GSIRouter vers la partitionUSR#kp de ByActorÉcrire les attributsprojetés

L'appelant reçoit son 200 OK à l'étape trois, avant que les étapes quatre à six ne finissent — donc un Query sur ByActor dans cet intervalle peut manquer un événement tout neuf.

Cette asynchronie est intentionnelle, pas un défaut : c'est la lignée du papier Amazon Dynamo de 2007, qui a choisi la disponibilité plutôt que la cohérence synchrone. Les conséquences complètes vivent dans pourquoi un GSI est à cohérence à terme.

La clé du GSI n'est pas une clé unique

En SQL, un index secondaire non unique est le défaut et un unique est une contrainte que tu choisis. Un GSI est l'inverse : il n'a aucune garantie d'unicité, jamais.

Deux événements d'audit du même acteur à des timestamps qui collisionnent partageraient le même GSI1PK et GSI1SK. DynamoDB stocke les deux — il les désambiguïse en interne par la clé primaire de la table de base, qui est toujours emportée avec.

Donc un Query de GSI pour un acteur à un instant peut légitimement renvoyer plusieurs items. Si tu as supposé une-ligne-par-clé comme un index unique SQL te le donnerait, c'est le piège.

Quand tu interroges l'index, le constructeur d'expressions DynamoDB écrit la KeyConditionExpression avec les noms et valeurs échappés correctement — par ex. correspondant à un acteur depuis un seuil :

KeyConditionExpression: "#a = :actor AND #ts > :since"
ExpressionAttributeNames:  { "#a": "actorId", "#ts": "EventSK" }
ExpressionAttributeValues: {
  ":actor": { "S": "USR#kp" },
  ":since": { "S": "TS#2026-06-01T00:00:00Z" }
}

La capacité vit avec l'index, pas avec la table

Parce que le GSI est sa propre table, il a sa propre capacité de lecture et d'écriture, facturée et throttlée séparément de la table de base. Une lecture sur ByActor consomme les unités de lecture du GSI, jamais celles de la table.

Le couplage inverse est celui qui mord : chaque écriture de table de base écrit aussi l'index, et si le GSI ne peut pas l'absorber, il fait pression en retour sur l'écriture de base. Ce mécanisme a son propre guide — quand un GSI throttle les écritures de la table de base.

C'est aussi pourquoi la clé de partition d'un GSI importe autant que celle de la table de base. Une clé de GSI à faible cardinalité regroupe les écritures sur une seule partition d'index même quand les écritures de base sont parfaitement réparties — une partition chaude que tu as créée en re-clé.

Pièges et étapes suivantes

  • Ne t'attends pas à récupérer les attributs non projetés. Un Query de GSI ne renvoie que ce que l'index stocke. Si tu as besoin de l'item complet, projette-le ou récupère-le depuis la table de base par les clés emportées.
  • Ne traite pas une clé de GSI comme unique. Prévois qu'un Query renvoie plus d'un item par clé ; la clé primaire de base est la seule vraie identité.
  • Ne lis pas un GSI juste après l'écriture qui l'a alimenté. Le chemin async signifie que l'index peut ne pas montrer ton écriture encore — lis la table de base quand tu as besoin de lire tes propres écritures.
  • Dimensionne la capacité du GSI délibérément. Elle est indépendante en lecture et une dépendance cachée en écriture.

Tout le jeu consiste à choisir des formes de clé qui servent tes patterns — le single-table design surcharge un GSI sur plusieurs d'entre eux ; GSI vs LSI couvre quand un index local convient à la place.

Construis et prévisualise ta KeyConditionExpression de GSI dans le constructeur d'expressions DynamoDB, puis essaie DynoTable pour inspecter les attributs projetés d'un index et regarder les écritures se répliquer dans le GSI sur tes propres tables.

Mis à jour