Intermédiaire6 min de lecture

Le zero-padding des clés de tri dans DynamoDB

Une clé de tri string DynamoDB trie lexicographiquement — un caractère à la fois, de gauche à droite — pas numériquement. Donc "10" atterrit avant "2", parce que "1" vient avant "2". Le zero-padding à largeur fixe est la façon dont tu fais correspondre l'ordre des chaînes à l'ordre numérique.

Pourquoi « 10 » trie-t-il avant « 2 » dans une clé de tri DynamoDB ?

Parce qu'une clé de tri de type string dans DynamoDB est comparée lexicographiquement par ordre d'octets UTF-8, pas numériquement. L'octet de "1" précède "2", donc "10" atterrit avant "2". Pad chaque nombre à une largeur fixe avec des zéros en tête — "2" devient "0000000002" — et l'ordre des chaînes correspond alors exactement à l'ordre numérique.

  • Le piège : les nombres stockés en chaînes trient comme des mots. "100", "11", "2" est l'ordre que DynamoDB te donne — pas ce que tu voulais dire.
  • La solution : pad chaque nombre à une largeur fixe avec des zéros en tête, pour que "2" devienne "0000000002". Maintenant l'ordre lexicographique et numérique s'accordent.
  • Choisis une largeur une fois : dimensionne-la pour la plus grande valeur que tu stockeras jamais, puis ajoute quelques chiffres. Changer la largeur plus tard signifie réécrire chaque clé.
  • Décroissant gratuitement : pour trier du haut vers le bas (le cas du classement), stocke maxValue - value, aussi zero-paddé — DynamoDB n'a pas de sens de tri par attribut.

Pourquoi les clés de tri string te trahissent

En venant de SQL, un ORDER BY score DESC sur une colonne entière « fonctionne tout seul » — le moteur sait que la colonne est numérique. DynamoDB n'a pas ce luxe pour une clé de tri qui n'est pas de type Number.

DynamoDB compare les clés de tri string (S) par ordre des octets UTF-8, selon la documentation AWS sur les clés de tri. Des octets, pas une magnitude. "9" (0x39) surclasse "10" parce que son premier octet bat "1" (0x31). La longueur est sans importance — seul le premier octet qui diffère décide.

C'est ça le footgun : à l'instant où un nombre vit dans une clé de tri string, chaque Query qui parcourt la plage renvoie les lignes dans un ordre qui semble brouillé.

Construire une clé de tri de classement

Prends un classement d'arcade saisonnier. Une collection d'items par saison contient chaque run de joueur, et tu veux les meilleurs scores en premier.

Modélise-le avec une clé composite dans une seule collection d'items :

  • leaderboardId (clé de partition) — p. ex. SEASON#2026-SPRING.
  • rankKey (clé de tri) — le score zero-paddé plus un départage.

Une première tentative naïve stocke le score brut sous forme de chaîne :

leaderboardIdrankKeyplayerHandle
SEASON#2026-SPRING"9"quickdraw
SEASON#2026-SPRING"10"ace_pilot
SEASON#2026-SPRING"1500"nightowl
SEASON#2026-SPRING"240"bytecrash

Un Query sur SEASON#2026-SPRING les renvoie dans cet ordre d'octets : "10", "1500", "240", "9". Le run à 9 points se retrouve bon dernier et le run à 1500 points est enterré au milieu. Inutile pour un classement.

Pad à une largeur fixe

Choisis une largeur assez large pour le plus grand score que tu enregistreras jamais, puis pad à gauche avec des zéros. Disons que les scores plafonnent à dix millions — c'est huit chiffres, alors utilise dix chiffres pour la marge :

leaderboardIdrankKeyplayerHandle
SEASON#2026-SPRING"0000000009"quickdraw
SEASON#2026-SPRING"0000000010"ace_pilot
SEASON#2026-SPRING"0000000240"bytecrash
SEASON#2026-SPRING"0000001500"nightowl

Maintenant chaque clé fait la même longueur, donc la comparaison octet par octet et la comparaison numérique produisent un ordre identique. Un Query ascendant donne 9, 10, 240, 1500. Les maths correspondent enfin aux octets.

La largeur est une porte à sens unique. Si tu pad à dix chiffres et qu'un score dépasse ça plus tard, une valeur à 11 chiffres trie avant une à 10 chiffres — cassant tout à nouveau — et la corriger signifie réécrire chaque rankKey existante. Sur-provisionne la largeur ; le coût est une poignée d'octets.

Trier décroissant : stocker la différence

Un classement veut le score le plus élevé en premier. DynamoDB peut lire une clé de tri vers l'avant ou vers l'arrière avec ScanIndexForward: false, donc le décroissant est généralement un drapeau à la lecture — atteins ça en premier.

Mais quand une collection d'items doit servir des sens de tri mêlés, ou que tu veux le meilleur score physiquement en premier indépendamment des drapeaux de lecture, inverse le nombre lui-même. Stocke maxValue - score, zero-paddé à la même largeur :

score   inverted (9999999999 - score)   rankKey
1500    9999998499                       "9999998499"
240     9999999759                       "9999999759"
10      9999999989                       "9999999989"
9       9999999990                       "9999999990"

L'ordre des octets ascendant sur la valeur inversée donne maintenant les scores originaux du plus haut au plus bas : 1500, 240, 10, 9. L'astuce est dans l'esprit du papier Amazon Dynamo de 2007 — les clés sont des octets opaques, donc tu encodes l'intention dans les octets.

Ajouter un départage

Deux joueurs peuvent être à égalité. Un score paddé nu entre en collision sur la clé de tri, et une seconde écriture écraserait la première (même PK + SK). Ajoute un suffixe unique pour que chaque run soit un item distinct et que les égalités se résolvent de façon déterministe :

rankKey = "<paddedScore>#<paddedTimestamp>#<playerId>"

Par exemple "0000001500#0000001719100800#p_8842". À score égal, le timestamp plus ancien gagne la meilleure place — pad aussi le timestamp, sinon il réintroduit exactement le bug que tu viens de corriger.

Dans DynoTable, tu peux parcourir le classement de saison trié par la rankKey zero-paddée et observer les valeurs remplies aligner correctement les lignes — preuve que les largeurs sont bonnes avant de les déployer.

Quand tu assembles cette clé composite à la main, il est facile de se tromper d'un chiffre de largeur. Générer le KeyConditionExpression pour un Query « haut de la saison » dans l'expression builder garde la syntaxe begins_with / between honnête pendant que tu expérimentes les largeurs.

Parcours du classement de saison dans DynoTable, trié par rankKey zero-paddée.
Parcours du classement de saison dans DynoTable, trié par rankKey zero-paddée.

Pièges à éviter

  • Padding trop étroit. Tout le schéma s'effondre la première fois qu'une valeur déborde la largeur. Dimensionne pour le pire cas, puis ajoute des chiffres.
  • Oublier le drapeau de lecture. Si tu lis seulement en décroissant, ScanIndexForward: false peut suffire — n'atteins pas les clés inversées quand un drapeau le fait.
  • Largeurs mêlées dans une collection. Chaque clé partageant une plage de tri doit utiliser la même largeur. Une migration qui pad les nouvelles lignes mais pas les anciennes les entrelace mal.
  • Padder le mauvais segment. Dans une clé composite, pad chaque segment numérique qui participe à l'ordre — le score et le timestamp tous les deux, pas seulement le score.

Étapes suivantes

Le zero-padding est un outil dans la boîte plus large de conception de clé de tri ; associe-le aux collections d'items quand tu surcharges une clé pour servir plusieurs modes, et appuie-toi sur un Query précis plutôt qu'un Scan une fois l'ordre correct.

Essaie DynoTable pour parcourir une vraie table et observer tes clés de tri zero-paddées tomber dans l'ordre numérique avant de livrer le schéma.

Mis à jour