Intermediário6 min de leitura

Chaves de Classificação com Zero-Padding no DynamoDB

Uma chave de classificação de string no DynamoDB ordena lexicograficamente — um caractere por vez, da esquerda para a direita — não numericamente. Então "10" cai antes de "2", porque "1" vem antes de "2". Fazer zero-padding até uma largura fixa é como você faz a ordem de string corresponder à ordem numérica.

Por que "10" ordena antes de "2" em uma chave de classificação do DynamoDB?

Porque uma chave de classificação de string no DynamoDB é comparada lexicograficamente pela ordem de bytes UTF-8, não numericamente. O byte de "1" precede "2", então "10" cai antes de "2". Preencha cada número até uma largura fixa com zeros à esquerda — "2" vira "0000000002" — e a ordem de string passa a corresponder exatamente à ordem numérica.

  • A cilada: números armazenados como strings ordenam como palavras. "100", "11", "2" é a ordem que o DynamoDB te dá — não o que você quis dizer.
  • A correção: preencha cada número até uma largura fixa com zeros à esquerda, para que "2" vire "0000000002". Agora a ordem lexicográfica e a numérica concordam.
  • Escolha uma largura uma vez: dimensione-a para o maior valor que você jamais armazenará, depois adicione alguns dígitos. Mudar a largura depois significa reescrever toda chave.
  • Descendente de graça: para ordenar do maior para o menor (o caso do leaderboard), armazene maxValue - value, também com zero-padding — o DynamoDB não tem direção de classificação por atributo.

Por que chaves de classificação de string te traem

Vindo do SQL, um ORDER BY score DESC sobre uma coluna inteira "simplesmente funciona" — o engine sabe que a coluna é numérica. O DynamoDB não tem esse luxo para uma chave de classificação que não seja do tipo Number.

O DynamoDB compara chaves de classificação de string (S) por ordem de bytes UTF-8, conforme a documentação de chave de classificação da AWS. Bytes, não magnitude. "9" (0x39) supera "10" porque seu primeiro byte ganha de "1" (0x31). O comprimento é irrelevante — só o primeiro byte diferente decide.

Essa é a cilada: no momento em que um número vive dentro de uma chave de classificação de string, todo Query que percorre o intervalo retorna linhas em uma ordem que parece embaralhada.

Construa uma chave de classificação de leaderboard

Pegue um leaderboard de arcade sazonal. Uma coleção de itens por temporada contém a corrida de cada jogador, e você quer os melhores scores primeiro.

Modele-o com uma chave composta em uma única coleção de itens:

  • leaderboardId (chave de partição) — ex.: SEASON#2026-SPRING.
  • rankKey (chave de classificação) — o score com zero-padding mais um critério de desempate.

Uma primeira tentativa ingênua armazena o score cru como string:

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

Um Query em SEASON#2026-SPRING os retorna nesta ordem de bytes: "10", "1500", "240", "9". A corrida de 9 pontos fica em último e a de 1500 pontos está enterrada no meio. Inútil para um leaderboard.

Preencha até uma largura fixa

Escolha uma largura ampla o suficiente para o maior score que você jamais registrará, depois preencha à esquerda com zeros. Digamos que os scores cheguem a dez milhões — são oito dígitos, então use dez dígitos por folga:

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

Agora toda chave tem o mesmo comprimento, então a comparação byte a byte e a comparação numérica produzem a ordem idêntica. Um Query ascendente dá 9, 10, 240, 1500. A matemática finalmente corresponde aos bytes.

A largura é uma porta de mão única. Se você preenche até dez dígitos e um score depois excede isso, um valor de 11 dígitos ordena antes de um de 10 dígitos — quebrando tudo de novo — e consertar isso significa reescrever toda rankKey existente. Superprovisione a largura; o custo é um punhado de bytes.

Ordenar descendente: armazene a diferença

Um leaderboard quer o score mais alto primeiro. O DynamoDB pode ler uma chave de classificação para frente ou para trás com ScanIndexForward: false, então descendente geralmente é uma flag em tempo de leitura — busque-a primeiro.

Mas quando uma coleção de itens precisa servir direções de classificação mistas, ou você quer o melhor score fisicamente primeiro independentemente das flags de leitura, inverta o próprio número. Armazene maxValue - score, com zero-padding na mesma largura:

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

A ordem de bytes ascendente sobre o valor invertido agora produz os scores originais do maior para o menor: 1500, 240, 10, 9. O truque está no espírito do paper Amazon Dynamo de 2007 — chaves são bytes opacos, então você codifica a intenção dentro dos bytes.

Adicione um critério de desempate

Dois jogadores podem empatar. Um score preenchido sozinho colide na chave de classificação, e uma segunda escrita sobrescreveria a primeira (mesma PK + SK). Acrescente um sufixo único para que cada corrida seja um item distinto e os empates se resolvam deterministicamente:

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

Por exemplo "0000001500#0000001719100800#p_8842". Mesmo score, timestamp mais cedo ganha o slot mais alto — preencha o timestamp também, ou ele reintroduz exatamente o bug que você acabou de consertar.

No DynoTable, você pode navegar pelo leaderboard da temporada ordenado pela rankKey com zero-padding e observar os valores preenchidos alinhando as linhas corretamente — prova de que as larguras estão certas antes de você colocar em produção.

Ao montar essa chave composta à mão é fácil errar uma largura. Gerar a KeyConditionExpression para um Query de "topo da temporada" no expression builder mantém a sintaxe begins_with / between honesta enquanto você experimenta com larguras.

Navegando no leaderboard da temporada no DynoTable, ordenado pela rankKey com zero-padding.
Navegando no leaderboard da temporada no DynoTable, ordenado pela rankKey com zero-padding.

Ciladas a evitar

  • Preencher estreito demais. O esquema todo desmorona na primeira vez que um valor estoura a largura. Dimensione para o pior caso, depois adicione dígitos.
  • Esquecer a flag de leitura. Se você só lê descendente, ScanIndexForward: false pode ser tudo que você precisa — não recorra a chaves invertidas quando uma flag resolve.
  • Larguras mistas em uma coleção. Toda chave compartilhando um intervalo de classificação precisa usar a mesma largura. Uma migração que preenche linhas novas mas não as antigas as intercala de forma errada.
  • Preencher o segmento errado. Em uma chave composta, preencha todo segmento numérico que participa da ordenação — score e timestamp, ambos, não só o score.

Próximos passos

Zero-padding é uma ferramenta no kit mais amplo de design de chave de classificação; combine-o com coleções de itens quando você sobrecarrega uma chave para servir vários padrões, e apoie-se em um Query preciso em vez de um Scan assim que a ordenação estiver certa.

Experimente o DynoTable para navegar em uma tabela real e ver suas chaves de classificação com zero-padding caírem em ordem numérica antes de você lançar o schema.

Atualizado