Desnormalização no DynamoDB
Vindo do SQL, desnormalização soa como pecado — dados duplicados, sem fonte única da verdade. No DynamoDB é o objetivo todo. Não há joins, então você copia os dados relacionados para o item que precisa deles e os lê de volta de uma vez.
O que é desnormalização no DynamoDB?
Desnormalização no DynamoDB significa copiar os dados relacionados para o próprio item que os lê, de modo que uma única consulta retorne tudo de uma vez. Como o DynamoDB não tem joins, você pré-junta os dados na hora da escrita em vez de costurar tabelas na hora da leitura. A contrapartida é a defasagem — duplique apenas valores que raramente mudam.
- Sem joins significa pré-juntar na hora da escrita. Armazene o valor relacionado no item que o lê, para que uma consulta nunca precise de uma segunda busca.
- Dois sabores. Embuta dados aninhados em um atributo complexo em um item, ou duplique um valor em muitos itens.
- A cilada é a defasagem. Quando a fonte muda, toda cópia fica errada até você propagar a atualização. Só duplique valores que raramente mudam.
- Isso compra leituras, não escritas. Você troca escritas mais numerosas (e mais cuidadosas) por leituras baratas, de requisição única.
Por que não há joins em que se apoiar
Um JOIN relacional remonta linhas normalizadas na hora da leitura. O DynamoDB não
tem join — um Query lê uma coleção de itens e devolve exatamente o que está
armazenado ali. Nada costura duas tabelas para você.
Então os dados já têm que estar moldados para a leitura. Se uma tela precisa de um post e do nome do autor, esse nome precisa viver em algum lugar que a leitura do post já toque. O paper Amazon Dynamo de 2007 tornou essa troca explícita: abrir mão de recursos relacionais para obter leituras previsíveis, de milissegundos de um dígito, em escala.
Padrão 1 — embutir com um atributo complexo
Atributos do DynamoDB podem conter mapas e listas aninhados, não só escalares. Então uma forma comum de desnormalização é enfiar um objeto filho diretamente dentro do item pai em vez de dar a ele o próprio item.
Um post com suas tags e um pequeno snapshot do autor, tudo em um item:
| PK | SK | author | tags |
|---|---|---|---|
| POST#9f3 | META | {id: U#12, name: "Mara Vance"} | ["dynamodb","aws"] |
Um GetItem retorna o post, as tags e o bloco do autor juntos. Sem segunda leitura.
Isso é ótimo para dados pertencentes ao pai e limitados em tamanho — um punhado de
tags, um snapshot de autor.
O limite a respeitar: um único item do DynamoDB chega no máximo a 400 KB, nomes e valores de atributos incluídos (Service Quotas). Embuta uma lista ilimitada (todo comentário de um post viral) e você vai estourar isso.
Padrão 2 — duplicar um valor entre itens
O caso do blog é o de manual. Você lista posts e quer que cada linha mostre o nome de exibição do autor — mas não quer uma segunda leitura por post para buscá-lo.
Então você escreve o nome do autor em cada item de post quando o post é criado:
| PK | SK | authorId | authorName | title |
|---|---|---|---|---|
| POST#9f3 | META | U#12 | "Mara Vance" | "Modeling 1:N" |
| POST#a71 | META | U#12 | "Mara Vance" | "Sparse GSIs" |
| POST#b04 | META | U#88 | "Lio Tan" | "Query vs Scan" |
Agora Query PK begins_with "POST#" (ou um GSI sobre posts) renderiza a lista inteira
— título e autor — sem busca por linha. O nome do autor está desnormalizado: a
cópia canônica vive em USER#12, e cada post carrega sua própria cópia.
A troca está bem ali. Você transformou uma leitura N+1 em uma leitura, ao custo de
manter "Mara Vance" em N+1 lugares.
Embutir vs. duplicar — qual deles
| Embutir (atributo complexo) | Duplicar (copiar entre itens) | |
|---|---|---|
| Forma | filho aninhado dentro do pai | mesmo valor em muitos itens |
| Melhor para | dados limitados, do pai | um valor compartilhado que muitos itens exibem |
| Leitura | um GetItem | um Query |
| Custo de atualização | reescrever o único item pai | propagar para cada cópia |
| Risco de tamanho | limite de 400 KB por item | nenhum por item |
Recorra a embutir quando o filho só aparece com o pai. Recorra a duplicar quando muitos itens independentes precisam exibir o mesmo valor compartilhado.
A cilada: cópias defasadas
Eis a parte que morde. Mara se renomeia para "Mara V.". Você atualiza USER#12. Todo
item de post ainda diz "Mara Vance" até você ir consertá-los.
Então atualizar um valor duplicado é uma escrita em leque (fan-out), não uma linha única. Você consulta cada item afetado e reescreve cada um — idealmente protegido para que você só toque linhas que ainda têm o valor antigo:
UPDATE POST#9f3
SET authorName = "Mara V."
WHERE authorName = "Mara Vance"
Você pode compor esse SET condicional contra authorName no
Expression Builder e copiar a UpdateExpression
e a ConditionExpression geradas direto para o seu código.
O fan-out em si é uma escrita por item: consulte os posts do autor, depois emita as atualizações. A sequência:
O custo de duplicar dados: toda mudança na fonte é uma consulta mais uma escrita por cópia.
É por isso que a regra é só duplicar valores que raramente mudam. Um nome de exibição, um nível de plano, um rótulo de categoria — tudo bem. Um contador ao vivo ou um campo editado com frequência — não; o fan-out vai te comer vivo.
Quando a normalização ainda vence
Se um valor muda com frequência, ou um item é lido por padrões genuinamente imprevisíveis, mantenha-o normalizado e aceite a leitura extra. A desnormalização é uma otimização para padrões de acesso conhecidos e com leituras pesadas — não um padrão a aplicar em todo lugar. Pré-junte as leituras que você de fato roda, e deixe o resto em paz.
Para decidir onde esses atributos duplicados vivem, modele os padrões de acesso primeiro — veja single-table design e, para o lado de leitura da troca, Query vs Scan.
Baixe o DynoTable para inspecionar uma tabela desnormalizada, identificar quais cópias ficaram defasadas e rodar a atualização em leque contra seus próprios dados.