Curso
Bem-vindo ao mundo do DynamoDB, onde eficiência e escalabilidade praticamente ilimitada se encontram em uma única tabela. Neste guia completo, vamos embarcar em uma jornada para revelar todo o potencial do design de banco NoSQL com tabela única.
Descubra como simplificar modelos de dados complexos enquanto aproveita toda a força de uma estrutura unificada, pronta para acompanhar suas necessidades em constante crescimento.
Seja você um desenvolvedor experiente em bancos NoSQL ou esteja apenas começando nessa área, prepare-se para levar a escalabilidade dos seus aplicativos a um novo patamar com uma arquitetura de dados poderosa!
O que é NoSQL?
Antes de começar, é importante entender a diferença entre NoSQL e banco relacional, em quais cenários usar cada um e por que essas tecnologias foram criadas. Para isso, vale um breve olhar na história do processamento de dados.
Você pode ler mais sobre a importância dos bancos NoSQL na ciência de dados em um post separado.
Bancos relacionais
O banco relacional é uma tecnologia conhecida e amplamente utilizada desde a década de 1970. Dois dos principais problemas que ele resolve são armazenamento e integridade referencial.
Os dados são “normalizados”, geralmente até a terceira forma normal, o que reduz a pegada de armazenamento e garante que qualquer atualização permaneça consistente com o restante dos dados.
O tamanho da pegada de dados persistidos era crucial porque, historicamente, o componente mais caro no data center era o disco rígido. Hoje não é mais assim: o recurso mais caro é a CPU, e o custo de armazenamento caiu drasticamente. Isso não torna o banco relacional obsoleto; ele segue sendo adequado para:
- OLAP
- Data warehouses
- Consultas ad hoc
- Aplicações com padrões de acesso pouco definidos
- CMS / headless CMS
Bancos NoSQL
“Pressão de dados” é a capacidade de um sistema processar um determinado volume de dados com custo e/ou tempo razoáveis.
Os bancos NoSQL surgiram no fim dos anos 1990 para lidar com a pressão de dados que afetava o desempenho dos bancos relacionais diante do crescimento constante do volume de dados das aplicações.
A CPU em um banco relacional costuma ser um gargalo, já que o servidor precisa buscar e juntar dados normalizados para formar visões desnormalizadas que os aplicativos consomem.
Isso pode ser aliviado com réplicas de leitura, mas o NoSQL segue outro caminho: armazena os dados já desnormalizados, prontos para consumo pela aplicação. A pegada de dados fica maior do que em um banco relacional, porém a carga na CPU diminui, fazendo o servidor atuar como um roteador simples que usa um algoritmo de hash para apontar para o local no disco no data center. Usos típicos de bancos NoSQL incluem:
- OLTP
- Aplicações com padrões de acesso bem definidos
- Aplicações que precisam escalar horizontalmente para volumes regionais ou globais
Diversos estudos já nos deram dados objetivos suficientes para representar, em um gráfico, o desempenho de SQL vs. NoSQL em grande escala.

Um último ponto: os dados armazenados em um banco NoSQL ainda são dados relacionais. Se não fossem relacionais, guardaríamos em um simples repositório de arquivos.
Antipadrões em NoSQL
Não modelamos um banco NoSQL da mesma forma que um relacional, então é antipadrão traduzir um design normalizado diretamente para NoSQL usando várias tabelas com junções, porque NoSQL não tem operador de join nem integridade referencial.
Uma tabela em NoSQL é equivalente a um catálogo em um banco relacional. Em vez disso, modele os dados para ficarem desnormalizados, prontos para o consumo pela aplicação.
Outro antipadrão são as Hot Keys. Isso significa que a maior parte (ou todas) as requisições atingem um único nó de armazenamento — sinal de que o espaço de chaves não foi bem projetado.
O mapa de calor de acesso ficaria mais ou menos assim:

DynamoDB
O AWS DynamoDB é um serviço totalmente gerenciado, serverless, do tipo wide column key-value, o que significa que ele suporta muitos itens (linhas) em uma tabela, e esses itens não precisam ter os mesmos atributos. Os tipos de dados compatíveis são:
- Tipos escalares: string, number, binary, boolean, null
- Tipos de documento: estruturas complexas com atributos aninhados (ex.: JSON)
- Tipos de conjunto: string set, number set, binary set
Como já mencionado, NoSQL deve ser usado quando os padrões de acesso da aplicação são bem conhecidos e há necessidade de suportar um grande número de TPS. O DynamoDB pode escalar para qualquer carga e mantém tempos de resposta previsíveis e consistentes de até 4 milhões de TPS com baixa latência (10–20 ms).
Como o DynamoDB funciona?
Cada item (ou linha) tem uma partition key que o identifica de forma única e determina a distribuição dos dados no armazenamento subjacente.
Itens podem ter, opcionalmente, uma sort key, que define a ordem em que os dados são gravados em disco dentro de uma partição. As sort keys permitem consultar partições com operações de faixa e filtros mais complexos (embora os filtros sejam aplicados na camada do cliente, não no banco).
A partition key é usada para criar um índice de hash, e cada item é distribuído conforme esse hash em um espaço de chaves virtual. Esse espaço é dividido em segmentos que mapeiam para dispositivos de armazenamento físicos, podendo crescer e encolher dinamicamente conforme o volume de dados.
É por isso que o DynamoDB é rápido e consistente em qualquer escala: o mecanismo do banco atua como um serviço de roteamento de chaves “hasheadas” até o armazenamento físico.
Modelagem de dados com NoSQL
Neste tutorial, vamos modelar uma loja online chamada Daintree.com.
(Curiosidade: a floresta Daintree, em Queensland, Austrália, é uma das mais antigas do planeta — estima-se que tenha cerca de 180 milhões de anos!)
Primeiro, vamos criar um diagrama de relacionamento de entidades (ERD) bem simplificado para visualizar o que vamos modelar.

A partir disso, deduzimos que um cliente pode criar um carrinho e adicionar vários produtos com quantidades diferentes.
Um produto tem uma categoria, que por sua vez pode ter categorias-pai.
Carrinhos podem virar pedidos, que podem ser divididos em vários pagamentos.
Como já dito, essa é uma visão altamente simplificada dos relacionamentos e atributos das entidades que compõem um aplicativo de compras online, mas serve ao propósito deste tutorial.
Defina os padrões de acesso
Em um banco relacional, geralmente não nos preocupamos tanto com esta etapa. SQL é uma linguagem extremamente poderosa que, desde que o esquema esteja bem desenhado, nos dá flexibilidade para consultar e manipular dados como quisermos. Em NoSQL com design de tabela única não é assim: precisamos armazenar os dados desnormalizados e prontos para consumo direto pela aplicação.
Veja alguns padrões de acesso que podemos querer suportar na nossa aplicação de e-commerce:
- Consultar todos os produtos por categoria
- Consultar as subcategorias de uma categoria-pai
- Consultar todos os pedidos por cliente
- Consultar todos os pagamentos de um pedido
- Consultar um pedido específico de um cliente
- Consultar todos os itens do carrinho de um cliente
- Consultar todos os produtos pelo histórico de pedidos do cliente
Modelando a tabela
O NoSQL Workbench é uma ferramenta gratuita da AWS para modelar tabelas do DynamoDB.
Vá em Data modeler e crie um novo modelo com uma nova tabela:

Vamos manter os atributos da chave primária como strings genéricas PK e SK:

Para atributos adicionais, vamos armazenar o entity_type e dois pares de atributos de chave primária para GSI (PK e SK).
Um GSI é um global secondary index, que é uma cópia da sua tabela que o DynamoDB mantém sincronizada (de forma assíncrona!!) mas armazenada usando pares alternativos de chaves primárias. Esse é o mecanismo-chave de como uma única tabela suporta múltiplos padrões de acesso. Mais adiante falaremos disso; por ora, entenda que essas chaves guardarão as chaves primárias de entidades relacionadas.
Os outros atributos adicionais que precisamos adicionar são todos os atributos não-chave dos nossos dados (observe que esta etapa é apenas para a fase de design e não será explicitamente definida quando provisionarmos a tabela real):

Em seguida, adicionamos os dois GSIs usando as chaves que definimos na etapa anterior:

Clique em salvar e depois em visualize the data model. Você verá a tabela recém-criada, ainda vazia. Clique em edit table e, no canto superior direito, em edit data; isso permitirá adicionar novas linhas:

Prefixos de dados
Antes de começar a inserir dados, precisamos definir os prefixos de chave primária para cada entidade:
|
product |
p# |
|
category |
c# |
|
customer (user) |
u# |
|
basket |
b# |
|
basket item |
bi# |
|
order |
o# |
|
order line |
ol# |
|
payment |
py# |
|
invoice |
i# |
Modelando os dados
Dados do cliente
A maioria dos padrões de acesso gira em torno do cliente, então no índice primário usaremos o ID do cliente como partição principal, e o próprio registro do cliente será identificado com a sort key também sendo o ID do cliente.
Assim, podemos relacionar facilmente clientes a pedidos e clientes a carrinhos reutilizando a mesma partition key, mas com uma sort key exclusiva.
Nossa visão agregada fica assim:

Agora podemos consultar os três registros em um único comando, buscando toda a partição desse cliente:
export const getAllCustomerRecords = async (id: string) =>
dynamoClient
.query({
TableName: TABLE_NAME,
KeyConditionExpression: "pk=:pk",
ExpressionAttributeValues: {
":pk": valueToAttributeValue(addPrefix(id, CUSTOMER_PREFIX)),
},
})
.then((result) => result.Items);
À medida que a tabela cresce, esse tipo de consulta pode não ser a mais eficiente, pois retornará todo e qualquer pedido do cliente, além de outros registros como carrinhos e notas fiscais. Mas é bom saber que conseguimos acessar todos os dados do cliente em uma única query quando necessário.
Com um pequeno ajuste em ExpressionAttributeValues para incluir uma correspondência parcial (begins_with) na sort key, conseguimos consultar:
- Todos os registros de carrinho do cliente
- Todos os registros de pedidos do cliente
O mais importante aqui é entender como modelar relacionamentos um-para-um e um-para-muitos em NoSQL.
Categorias
Essas entidades representam outro relacionamento um-para-muitos, em que uma categoria pode conter muitos produtos.
Categorias também têm hierarquia: uma categoria pode ter várias subcategorias, e os produtos podem existir em qualquer nível dessa árvore.
Vamos modelar isso com livros, dividindo em ficção e não ficção.

Perceba como preenchemos as chaves de atributos do GSI para as subcategorias, definindo GSI1_PK como o ID da categoria-pai e GSI1_SK como o ID da subcategoria. Isso nos permite consultar todas as subcategorias por categoria usando o GSI1:

O mesmo índice, GSI1, pode ser usado não apenas para mapear relacionamentos um-para-muitos. Suponha que queiramos buscar o cliente pelo e-mail; o modelo atual não suporta isso diretamente, a menos que façamos um pequeno ajuste na forma de salvar os dados:

Ao armazenar o e-mail do cliente e o tipo de entidade como PK e SK do GSI, nós:
- Garantimos que o e-mail do cliente seja único (somente para esse GSI; impor exclusividade no índice primário exigiria outra solução)
- Permitimos que clientes também apareçam como sellers, por exemplo (se tivéssemos esse tipo de entidade)
- Passamos a buscar um cliente por e-mail em vez de por ID
Para ajudar na distribuição de dados do DynamoDB, poderíamos ir além e armazenar o e-mail como um hash.
export const getCustomerByEmail = async (email: string) =>
dynamoClient
.query({
TableName: TABLE_NAME,
IndexName: "gsi1",
KeyConditionExpression: "gsi1_pk = :email AND gsi1_sk = :entityType",
ExpressionAttributeValues: {
":email": valueToAttributeValue(email),
":entityType": valueToAttributeValue(entityType),
},
})
.then(({ Items }) => Items?.[0]);
Até aqui, talvez você tenha se perguntado por que todos os índices e atributos de chave têm nomes genéricos, como PK e SK. A resposta agora deve estar clara: os dados que armazenamos e os índices que mantemos são flexíveis e capazes de suportar múltiplos padrões de acesso, dependendo do tipo de entidade.
O Terraform da tabela que usamos até agora é o seguinte:
resource "aws_dynamodb_table" "tutorial-1" {
name = "tutorial-1"
billing_mode = "PAY_PER_REQUEST"
hash_key = "pk"
range_key = "sk"
attribute {
name = "pk"
type = "S"
}
attribute {
name = "sk"
type = "S"
}
attribute {
name = "gsi1_pk"
type = "S"
}
attribute {
name = "gsi1_sk"
type = "S"
}
attribute {
name = "gsi2_pk"
type = "S"
}
attribute {
name = "gsi2_sk"
type = "S"
}
global_secondary_index {
name = "gsi1"
hash_key = "gsi1_pk"
range_key = "gsi1_sk"
projection_type = "ALL"
}
global_secondary_index {
name = "gsi2"
hash_key = "gsi2_pk"
range_key = "gsi2_sk"
projection_type = "ALL"
}
}
Produtos
Armazenar produtos segue o mesmo padrão um-para-muitos: a partition key é o ID da categoria e a sort key é o ID do produto:

Adicionar a categoria-pai ao GSI1 nos permite consultar todos os livros do nosso banco:

Em produção, podemos usar DynamoDB Streams para enviar dados a um cluster Elasticsearch e, assim, buscar por título do livro. Mas como usar nosso modelo atual para consultar por título do produto?
Já usamos o GSI1, então poderíamos usar o GSI2 para isso:

Lembre-se: ao consultar o DynamoDB, você precisa fornecer a partition key completa e exata. Portanto, embora essa implementação possa não ser a mais otimizada, ela ilustra como três índices podem atender a diferentes padrões de acesso.
No meu próximo tutorial, vou mostrar como resolver isso integrando a um índice de busca.
Conteúdo do carrinho e linhas do pedido
Tanto os itens do carrinho quanto as linhas do pedido ficam na partição do cliente; podemos usar os GSIs para consultar todos os produtos em um carrinho, todos os produtos de um pedido e todos os produtos que um cliente já comprou.



Este último exemplo mostra o histórico de pedidos de um cliente com dois produtos feitos em dois pedidos diferentes.
Para quem se interessa por modelagem de dados em bancos SQL, nosso webinar Data Modeling in SQL traz ótimos insights que complementam o entendimento de modelagem NoSQL.
Desafios do design com tabela única
Usar uma tabela única com DynamoDB traz muitas vantagens, mas também alguns desafios e pontos de atenção:
Complexidade e lógica da aplicação
Concentrar todos os dados em uma única tabela pode se tornar complexo, especialmente à medida que sua aplicação cresce e evolui.
Você precisa planejar e estruturar os dados com cuidado para acomodar diversos padrões de consulta. Além disso, a eficácia do design de tabela única está intimamente ligada à lógica da aplicação, que é crucial para garantir acesso fluido e consultas eficientes.
Pensar bem tanto na estrutura dos dados quanto na lógica da aplicação é essencial para esse design funcionar.
Sobrecarga de índices
Embora o DynamoDB permita Global Secondary Indexes para suportar diferentes padrões de consulta, é preciso provisioná-los e mantê-los, o que pode aumentar custos e complexidade.
Escolha da chave de partição
Escolher a partition key correta é fundamental para distribuição uniforme dos dados e bom desempenho. Uma escolha ruim pode gerar partições quentes e gargalos.
Tamanho e custo
Com o aumento do volume, a sua tabela única pode crescer bastante. Isso afeta tanto o custo de armazenamento quanto o custo de leitura e escrita.
Sem busca full-text
O DynamoDB não oferece busca full-text nativa, então implementar isso exige integrar com serviços como Elasticsearch.
Mudanças de esquema
Alterar o esquema da tabela pode ser desafiador, especialmente em produção. Você pode precisar migrar dados existentes ou adaptar a lógica da aplicação.
Considerações de consistência
Dependendo do tipo de leitura (forte ou eventual), pode haver desafios de consistência, lembrando que GSIs são replicados de forma assíncrona.
Flexibilidade limitada de consulta
As consultas do DynamoDB são poderosas, mas não tão flexíveis quanto SQL.
Para entender melhor como projetar bancos de dados e navegar por esses desafios, confira nosso curso de Database Design.
Conclusão
Ao trabalhar com um banco NoSQL, é fundamental se desprender dos padrões familiares de design relacional — é preciso “desaprender” alguns hábitos.
Optar pelo design de tabela única oferece a promessa de um banco escalável globalmente, pronto para lidar com alto tráfego com desempenho previsível e confiável.
Mas essa vantagem não vem de graça: exige planejamento minucioso e uma implementação cuidadosa da estrutura do banco e da lógica da aplicação desde o início.
Aprofunde seu conhecimento com nosso curso de NoSQL concepts!
Sou engenheiro de software de pilha completa e arquiteto de soluções, apaixonado por aprendizado e dados!



