A pergunta não é "quais entidades existem?" — é "quais perguntas a aplicação fará?"

Modelagem de Dados NoSQL a fundo

Quase todo erro caro em NoSQL é de modelagem: array que estoura, partição quente, consulta que vira scatter-gather, migração que trava o banco por horas. Esta apostila trata a modelagem como disciplina — dos padrões de acesso ao single-table design, coluna larga, grafo e séries temporais, até a evolução de schema em produção.

10 módulosaccess-pattern-firstcatálogo de padrõessingle-table designcoluna larga & grafoexercícios com gabarito
💡 Onde esta apostila se encaixa

A apostila de NoSQL apresenta as famílias; a de MongoDB aprofunda um banco. Esta é transversal: o método de modelar, que vale para documento, chave-valor, coluna larga e grafo. Onde a teoria distribuída (CAP, quóruns, sharding) aparecer, é resumo — aprofunde nos módulos 3 e 7 da apostila de NoSQL.

MÓDULO 01 · BÁSICO

Access-pattern-first: a virada mental

Objetivo: entender por que a modelagem relacional (normalizar entidades) e a modelagem NoSQL (moldar dados por consulta) são processos invertidos — e o que isso implica na prática.

1.1 O processo relacional (que você provavelmente já tem no reflexo)

  1. Identificar entidades e relacionamentos (ERD).
  2. Normalizar (1FN, 2FN, 3FN) para eliminar redundância.
  3. Criar tabelas e chaves estrangeiras.
  4. Escrever as consultas depois — o otimizador e os JOINs se viram.

Isso funciona porque um banco relacional resolve qualquer consulta razoável sobre um schema normalizado, ao custo de JOINs em tempo de execução.

1.2 O processo NoSQL

  1. Levantar todos os padrões de acesso da aplicação (as perguntas que ela fará), com frequência e volume estimados.
  2. Projetar as estruturas de armazenamento para que cada padrão seja atendido com uma operação barata (idealmente uma leitura por chave, sem JOIN, sem varredura).
  3. Aceitar duplicação de dados como preço da leitura rápida.
  4. Só então nomear coleções/tabelas/chaves.
🔁 A inversão em uma frase

No relacional, você modela os dados e as consultas se adaptam. No NoSQL, você modela as consultas e os dados se adaptam. Se você desenhou um ERD normalizado e "traduziu" para coleções, provavelmente modelou errado.

1.3 Por que a duplicação deixa de ser pecado

💼 Mercado de trabalho

Entrevistadores de backend/dados abrem com "como você modelaria X em NoSQL?" e avaliam o processo: você pergunta pelos padrões de acesso antes de desenhar? Justifica cada duplicação? Reconhece o risco de dado mutável compartilhado? Quem começa desenhando um ERD normalizado perde pontos nos primeiros 30 segundos.

✏️ Exercício 1 — Identifique o erro de processo

Um time modelou um app de delivery assim: coleções usuarios, restaurantes, pratos, pedidos, itens_pedido, avaliacoes, cada uma com referências às outras, "igual ao banco relacional que a gente já tinha". A tela principal (histórico de pedidos do usuário) faz 5 consultas encadeadas e está lenta. Qual foi o erro e qual a direção da correção?

Gabarito: O erro foi de processo: traduziram um ERD normalizado para coleções em vez de modelar a partir dos padrões de acesso. A tela de histórico precisa, por pedido: itens (com nome e preço do prato no momento da compra), nome do restaurante, status. Correção: o documento pedido embute os itens com extended reference (nome do prato, preço congelado, nome do restaurante), de modo que a tela seja uma consulta por usuarioId com índice { usuarioId: 1, criadoEm: -1 }. pratos e restaurantes continuam como coleções próprias para as telas que os editam; o pedido não depende delas em leitura.

MÓDULO 02 · BÁSICO

Levantar os padrões de acesso

Objetivo: produzir o artefato central da modelagem NoSQL — a tabela de padrões de acesso — e usá-la como contrato entre produto, backend e dados.

2.1 A tabela de padrões de acesso

Antes de qualquer schema, liste cada operação de leitura e escrita que a aplicação faz. Para cada uma: o que entra (parâmetros), o que sai, com que frequência, em que volume, e qual latência é aceitável.

#Padrão de acessoChave de entradaRetornoFreq.Obs.
1Ver perfil do usuáriouserId1 doc de perfilaltap99 < 20 ms
2Listar pedidos do usuário (paginado, recentes primeiro)userId, cursor20 pedidosaltaordenar por data desc
3Ver um pedidoorderId1 pedido com itensmédia
4Listar pedidos de um restaurante por dia (painel)restId, diaN pedidosbaixauso interno
5Marcar pedido como entregueorderIdmédiaescrita idempotente

2.2 De onde tirar os padrões

2.3 Classificar cada padrão

EixoPerguntas
SeletividadeBusca 1 item por chave? Uma faixa ordenada? Um agregado? Uma busca textual?
Cardinalidade do resultadoRetorna 1, dezenas, milhares, milhões?
Frequência × volume1000 req/s de 1 item, ou 1 req/dia de 10 M de itens? Molda decisões opostas.
Consistência exigidaPrecisa ver a própria escrita na hora? Tolera segundos de atraso?
Padrão de escrita associadoQuem escreve esse dado, com que frequência, e o que precisa ficar consistente com ele?
💡 O teste do "one-shot"

Para cada padrão de acesso de alta frequência, pergunte: "consigo atender isto com uma única operação por uma chave que eu tenho em mãos, sem varrer e sem JOIN?" Se a resposta é não, o schema ainda não está pronto para esse padrão — ou ele é raro o bastante para tolerar o custo.

💼 Mercado de trabalho

Em system design, comece sempre escrevendo a lista de padrões de acesso no quadro antes de desenhar qualquer coisa. É o sinal nº 1 de que você sabe modelar NoSQL, e transforma a conversa de "qual banco?" para "quais garantias cada acesso exige?". O livro The DynamoDB Book formaliza isso; entrevistadores esperam ver.

✏️ Exercício 2 — Monte a tabela

Para um app de biblioteca (usuários pegam livros emprestados), liste ao menos 6 padrões de acesso com chave de entrada, retorno e frequência estimada. Inclua ao menos um job/relatório.

Gabarito (exemplo): (1) ver livro por isbn — 1 doc, alta; (2) buscar livros por título/autor — busca textual, média; (3) ver os empréstimos ativos de um usuário por userId — dezenas, alta; (4) ver o histórico de um exemplar por copyId — dezenas, baixa; (5) registrar empréstimo/devolução por copyId — escrita, média, idempotente; (6) job diário: listar todos os empréstimos vencidos (varre por dueDate < hoje e status = ativo) — milhares, 1×/dia; (7) relatório mensal: livros mais emprestados por categoria — agregação, 1×/mês. Observação-chave: (2) pede motor de busca; (6) pede um índice em { status, dueDate } ou uma coleção/índice esparso só de ativos.

MÓDULO 03 · INTERMEDIÁRIO

Embedding vs referencing decidido

Objetivo: transformar "depende" numa decisão defensável, cruzando cardinalidade, taxa de mudança, fan-out de atualização e limites físicos.

3.1 As quatro variáveis

VariávelPuxa para embeddingPuxa para referencing
Padrão de acessolidos juntos quase sempreacessados/atualizados de forma independente
Cardinalidade1:1, 1:poucos com teto conhecido1:muitos que cresce; 1:bilhões
Taxa de mudança do lado "um"muda raramente (ou é imutável/histórico)muda com frequência e é compartilhado por muitos
Limite físicosubdocumento pequeno e limitadodocumento se aproximaria do teto de tamanho; array ilimitado

3.2 O problema do fan-out de atualização

Se você embute o nome do usuário em cada comentário e ele tem 2 milhões de comentários, trocar o nome vira 2 milhões de escritas. Duas saídas de mercado:

3.3 O mapa por cardinalidade

RelaçãoExemploModelagem
1:1pedido ↔ dados de entregamesmo documento
1:poucos (teto ≤ ~dezenas)usuário ↔ endereços; produto ↔ variaçõesembedding em array, cada item com id próprio
1:muitos (cresce, mas por entidade cabe)post ↔ comentários; conta ↔ transações do mêsreferencing; ou subset (N recentes embutidos) + coleção; ou bucket
1:bilhõessensor ↔ leituras; usuário ↔ eventos de cliquecoleção própria, quase sempre coluna larga ou série temporal; nunca embutir
N:Naluno ↔ turmas; produto ↔ tagsarray de refs no lado que mais consulta; às vezes nos dois; adjacency list em single-table
⚠️ "Embute os 10 recentes" sem plano para o resto

O padrão subset só funciona se houver uma coleção referenciada com o histórico completo e um índice para paginá-lo. Embutir os 10 recentes e não guardar o resto em lugar nenhum é perda de dados disfarçada de otimização.

💼 Mercado de trabalho

A pergunta "embedding ou referencing para X?" nunca tem resposta única — o avaliador quer ver você citar as quatro variáveis e o fan-out de atualização. Mencionar "eu duplicaria o nome de exibição e propagaria mudanças por um change stream, mas nunca duplicaria papéis de permissão" é uma resposta de nível sênior.

✏️ Exercício 3 — Decida e justifique

Rede social. (a) Foto do post ↔ curtidas (podem chegar a milhões). (b) Post ↔ contagem de curtidas exibida. (c) Usuário ↔ nome de exibição, mostrado em cada post e comentário. (d) Post ↔ hashtags (até ~30).

Gabarito: (a) Referencing puro — 1:milhões, cresce sem teto; curtidas viram documentos { postId, userId, ts } com índice { postId: 1 } (e único { postId, userId } para evitar dupla). (b) Computed — campo curtidas no post, atualizado com incremento atômico na escrita da curtida; não conte a coleção a cada leitura. (c) Referencing por userId + resolução na leitura (batch por lista de ids, com cache); duplicar o nome em bilhões de itens cria fan-out proibitivo na troca de nome. Se duplicar mesmo assim (para evitar o batch), tem de haver propagação assíncrona. (d) Embedding — array de até 30 strings, lido sempre com o post, teto conhecido.

MÓDULO 04 · INTERMEDIÁRIO

O catálogo de padrões

Objetivo: conhecer pelo nome os padrões de modelagem que o mercado usa como vocabulário comum — para reconhecê-los, combiná-los e citá-los.

4.1 A tabela de referência

PadrãoProblemaSolução
Subsetrelação 1:muitos grande para embutir inteiraembute os N mais relevantes + coleção com o resto
Bucketmuitos itens pequenos por chave (séries temporais)1 documento agrupa uma janela fixa (1 h, 1 dia) num array
Computedrecalcular agregado na leitura é caropré-calcula na escrita (contadores, somas, médias)
Extended Referencesegundo fetch só para exibir um nomeduplica 2–3 campos estáveis do referenciado
Polymorphic"tipos" diferentes consultados juntosmesma coleção, campo tipo discrimina; campos variam por tipo
Attributemuitos campos parecidos e esparsos, cada um querendo índice (ex.: specs de produto)converte pares chave/valor em array de { k, v } e indexa k+v uma vez
Outlier99% dos docs pequenos, 1% giganteflag hasOverflow + coleção/estrutura à parte para os outliers
Tree / Hierarchyárvores (categorias, org chart, comentários aninhados)array de ancestrais (materialized path), ou parentRef, ou nested sets — escolha pelo padrão de leitura
Approximationcontador exato custa 1 escrita por eventoincrementa em lote / probabilístico (views, likes de baixa criticidade)
Schema Versioningmigrar coleção enorme sem downtimecampo schemaVersion + código tolerante + backfill
Envelope / document-per-Xgranularidade do documentoescolher a "unidade" (1 doc por dia, por sessão, por agregado) que casa com os acessos

4.2 Padrões de árvore — o mais errado na prática

// parentRef: fácil de escrever, caro para ler a subárvore inteira
{ _id: "c3", nome: "Tênis", parentId: "c1" }

// materialized path: leitura de subárvore e "breadcrumbs" viram 1 consulta por prefixo
{ _id: "c3", nome: "Tênis", path: ["c0", "c1"], pathStr: "/c0/c1/" }
// "toda a subárvore de c1": find({ path: "c1" })  — com índice em path

// array of ancestors + parentId: combina os dois; padrão recomendado pela MongoDB
{ _id: "c3", parentId: "c1", ancestors: ["c0", "c1"] }

Regra: se você lê subárvores inteiras com frequência, materialize o caminho; se só sobe um nível por vez, parentId basta; se a árvore quase nunca muda mas é lida o tempo todo, considere nested sets.

4.3 Combinar padrões é o normal

Um documento de post real pode usar Subset (comentários recentes) + Computed (contadores) + Extended Reference (autor) + Bucket (visualizações por dia) ao mesmo tempo. Os padrões não competem; compõem.

💼 Mercado de trabalho

Saber os padrões pelo nome é o vocabulário que MongoDB University, o MongoDB Blog e os livros de DynamoDB usam — e que entrevistadores esperam ouvir. "Aqui eu aplicaria o padrão attribute para não criar 40 índices esparsos" é o tipo de frase que diferencia.

✏️ Exercício 4 — Nomeie e combine

Catálogo de e-commerce com produtos de categorias muito diferentes (eletrônicos têm "voltagem", "resolução"; roupas têm "tamanho", "material"). A página do produto mostra: specs, as 3 avaliações mais recentes, a nota média, e o caminho de categorias (Eletrônicos › TVs › 4K). Que padrões você usa?

Gabarito: Attribute pattern para as specs (specs: [ { k: "voltagem", v: "220V" }, { k: "resolucao", v: "4K" } ] com um índice composto em { "specs.k": 1, "specs.v": 1 } — um índice serve a todas as specs). Subset para as 3 avaliações recentes embutidas + coleção avaliacoes. Computed para notaMedia e totalAvaliacoes. Materialized path (ou array of ancestors) para o caminho de categorias, permitindo os breadcrumbs numa leitura e "todos os produtos abaixo de TVs" com um filtro por prefixo.

MÓDULO 05 · INTERMEDIÁRIO → AVANÇADO

Single-table design (DynamoDB)

Objetivo: entender a técnica que assusta e depois convence — várias entidades numa tabela só — e quando ela vale (e quando não).

5.1 O modelo do DynamoDB em 90 segundos

5.2 Por que uma tabela só

Cada Query do DynamoDB lê uma partição de uma tabela. Se as entidades relacionadas (um pedido e seus itens, um usuário e seus endereços) compartilham a mesma PK, uma única Query traz o "agregado" inteiro — o equivalente ao JOIN, pré-computado pela colocação física.

// Item collection: tudo do pedido #123 sob a mesma PK
PK              SK                    ...atributos
ORDER#123       META#123              status, total, criadoEm
ORDER#123       ITEM#001              sku, nome, preco, qtd
ORDER#123       ITEM#002              sku, nome, preco, qtd
ORDER#123       SHIPMENT#001          transportadora, rastreio

// "traga o pedido 123 inteiro": Query(PK = "ORDER#123")  → 1 request

5.3 Key overloading e GSIs

Os nomes PK/SK são genéricos de propósito: cada tipo de item os preenche com um prefixo diferente (USER#, ORDER#, PRODUCT#). Para atender "todos os pedidos de um usuário, por data", cria-se um GSI:

// GSI1 — inverte a visão para consultar por usuário
GSI1PK          GSI1SK
USER#u1         ORDER#2026-06-04#123
USER#u1         ORDER#2026-05-21#118

// Query(GSI1PK = "USER#u1", GSI1SK begins_with "ORDER#")  ordenado por data
⚠️ Single-table design não é sempre a resposta

Ele otimiza custo e latência de padrões de acesso conhecidos e estáveis. Custa caro em: legibilidade (uma tabela com 8 tipos de item exige documentação rigorosa), flexibilidade (um novo padrão de acesso pode exigir um GSI ou um backfill), e ferramental (analytics ad-hoc precisa exportar para outro lugar). Para apps pequenos, times sem experiência em Dynamo, ou requisitos de consulta voláteis, várias tabelas (ou outro banco) podem ser a escolha madura.

💼 Mercado de trabalho

Single-table design é assunto de vaga sênior em stacks AWS e de entrevista de system design. Espera-se que você: desenhe a item collection, explique key overloading, use um sparse GSI como fila, e — crucialmente — saiba dizer quando não usar. O The DynamoDB Book (Alex DeBrie) é a referência que entrevistadores assumem.

✏️ Exercício 5 — Desenhe a tabela

App de eventos: um Evento tem metadados e uma lista de Ingressos vendidos; um Usuário tem os ingressos que comprou. Padrões: (1) ver um evento com seus ingressos; (2) ver os ingressos de um usuário; (3) listar eventos futuros por data. Proponha PK/SK e um GSI.

Gabarito: Tabela base — PK = EVENT#{id}, SK = META# (metadados) e SK = TICKET#{ticketId} (ingressos): Query(PK=EVENT#e1) traz o evento inteiro (padrão 1). Cada ingresso também carrega GSI1PK = USER#{userId}, GSI1SK = EVENT#{data}#{eventId}Query(GSI1PK=USER#u1) traz os ingressos do usuário ordenados por data do evento (padrão 2). Para o padrão 3, um GSI esparso: itens META# de eventos futuros recebem GSI2PK = "EVENT_UPCOMING", GSI2SK = {data}#{eventId}; um job remove o atributo quando o evento passa, mantendo o índice pequeno. Alternativa ao "hot partition" do EVENT_UPCOMING único: particionar por mês (GSI2PK = "UPCOMING#2026-06").

MÓDULO 06 · AVANÇADO

Coluna larga: tabela-por-consulta

Objetivo: modelar para Cassandra/ScyllaDB, onde a semelhança com SQL engana — a chave primária é uma decisão de armazenamento físico, não uma restrição lógica.

6.1 O modelo físico

PRIMARY KEY ( (partition_key) , clustering_col_1, clustering_col_2 )
                  └─ define o NÓ         └─ define a ORDEM dentro da partição

6.2 A regra: uma tabela por padrão de consulta

Se a aplicação consulta "mensagens por canal, recentes primeiro" e "mensagens por autor", são duas tabelas, alimentadas pela mesma escrita (desnormalização deliberada):

CREATE TABLE mensagens_por_canal (
  canal_id uuid, bucket text, msg_id timeuuid, autor_id uuid, texto text,
  PRIMARY KEY ( (canal_id, bucket), msg_id )
) WITH CLUSTERING ORDER BY (msg_id DESC);

CREATE TABLE mensagens_por_autor (
  autor_id uuid, msg_id timeuuid, canal_id uuid, texto text,
  PRIMARY KEY ( autor_id, msg_id )
) WITH CLUSTERING ORDER BY (msg_id DESC);

A aplicação escreve nas duas (idealmente com BATCH logged para as duas linhas relacionadas, ou aceitando reconciliação). Storage duplicado é o preço; leitura de partição única é o prêmio.

6.3 Os inimigos: partições grandes, hot partitions e tombstones

⚠️ Tratar CQL como SQL

CQL parece SQL, então dá vontade de fazer WHERE status = 'x', JOIN, GROUP BY. Cassandra recusa (ou exige ALLOW FILTERING, que varre nós). Se você precisa disso, ou faltou uma tabela desnormalizada, ou o caso não é de coluna larga.

💼 Mercado de trabalho

Em vagas de engenharia de dados e system design de escala (chat, feed, telemetria), espera-se: modelar "uma tabela por consulta", escolher partition key + clustering com o alvo de tamanho de partição em mente, e citar o problema dos tombstones. O design do chat do Discord (ScyllaDB, PRIMARY KEY ((channel_id, bucket), message_id)) é o case canônico.

✏️ Exercício 6 — Modele a telemetria

Dispositivos IoT enviam leituras a cada 10 s. Consultas: (1) últimas 24 h de um dispositivo; (2) todas as leituras de um dispositivo num dia específico; (3) valor mais recente de um dispositivo. Retenção: 90 dias. Proponha a(s) tabela(s) e a chave.

Gabarito: CREATE TABLE leituras (device_id uuid, dia date, ts timestamp, valor double, PRIMARY KEY ((device_id, dia), ts)) WITH CLUSTERING ORDER BY (ts DESC) AND default_time_to_live = 7776000; — partition key (device_id, dia) mantém a partição limitada (~8.640 linhas/dia) e distribui a escrita; clustering por ts DESC atende "últimas N" e faixas dentro do dia (padrões 1 e 2 — o padrão 1 lê hoje + ontem). Padrão 3 (valor mais recente): ou LIMIT 1 na partição do dia atual, ou uma tabela ultimo_valor (device_id PRIMARY KEY, ts, valor) sobrescrita a cada leitura. TTL de 90 dias evita DELETE e tombstones; alternativamente, descartar partições de dias antigos.

MÓDULO 07 · AVANÇADO

Modelagem de grafo

Objetivo: decidir o que vira nó, o que vira aresta e o que vira propriedade — e evitar os supernós que matam a performance de travessia.

7.1 Nó, aresta ou propriedade?

💡 O teste da travessia

Se você precisa perguntar "todos os X ligados a este Y" ou "o caminho entre A e B", isso é uma aresta. Se um atributo aparece nesse tipo de pergunta como filtro categórico de partida (ex.: "pessoas que moram em São Paulo"), promova-o a ((:Pessoa)-[:MORA_EM]->(:Cidade {nome:"São Paulo"})) — travessia por relação é mais rápida que filtro por propriedade.

7.2 Direção e tipo de aresta

Neo4j armazena arestas dirigidas, mas a travessia pode ignorar a direção. Modele a direção que faz sentido semântico (SEGUE, GEROU, PERTENCE_A) e consulte com ou sem direção conforme a pergunta. Prefira tipos específicos (:ENVIOU_MENSAGEM, :CURTIU) a um genérico :REL {tipo:"..."} — o motor indexa por tipo de aresta.

7.3 Supernós o problema nº 1

Um nó com milhões de arestas (o país "Brasil" ligado a todas as pessoas; a hashtag viral) faz qualquer travessia que o toque explodir. Mitigações:

7.4 Grafo de conhecimento (KG) e GraphRAG tema em alta

Um KG modela entidades e relações extraídas de texto/dados. Decisões de modelagem específicas: ontologia (quais tipos de nó e aresta são permitidos), resolução de entidades (o "Lula" do documento A e do documento B são o mesmo nó?), e proveniência (cada aresta guarda a fonte). O GraphRAG usa o KG para dar ao LLM contexto estruturado — a modelagem do KG é o que determina a qualidade das respostas. Aprofunde na apostila de IA Generativa & RAG e na de Grafos Aplicados.

💼 Mercado de trabalho

Modelagem de grafo aparece em vagas de fraude, recomendação, identidade e, cada vez mais, IA (KG + RAG). O que se testa: decidir nó vs aresta vs propriedade num domínio dado, reconhecer e mitigar supernós, e escrever a travessia (Cypher) correspondente. "Eu promoveria cidade a nó e inseriria estados como intermediários para não criar um supernó no país" é resposta de nível pleno/sênior.

✏️ Exercício 7 — Modele um antifraude

Domínio: contas, dispositivos, cartões, endereços de entrega. Sinal de fraude: várias contas "diferentes" compartilhando o mesmo dispositivo e cartão. Modele nós e arestas e descreva a consulta de detecção. Onde está o risco de supernó?

Gabarito: Nós: :Conta, :Dispositivo, :Cartao, :Endereco. Arestas: (:Conta)-[:USOU]->(:Dispositivo), (:Conta)-[:PAGOU_COM]->(:Cartao), (:Conta)-[:ENVIOU_PARA]->(:Endereco), com propriedade ultimoUso/primeiroUso nas arestas. Detecção: encontrar componentes conexos onde ≥ 3 :Conta se ligam ao mesmo :Dispositivo e ao mesmo :Cartao — em Cypher, algo como MATCH (c1:Conta)-[:USOU]->(d:Dispositivo)<-[:USOU]-(c2:Conta), (c1)-[:PAGOU_COM]->(k:Cartao)<-[:PAGOU_COM]-(c2) WHERE c1 <> c2 RETURN d, k, collect(distinct c1)+collect(distinct c2). Risco de supernó: dispositivos de datacenter/VPN e cartões de gift compartilhados legitimamente por milhares — trate com limiar (ignorar nós de grau > N) e/ou tipos de aresta particionados por janela de tempo.

MÓDULO 08 · AVANÇADO

Séries temporais e eventos

Objetivo: modelar dados que só crescem — leituras, logs, eventos de domínio — com bucketing, rollups, retenção e, quando faz sentido, event sourcing.

8.1 Bucketing e a granularidade do documento

Um documento por leitura (milhões/dia) é ineficiente: overhead por documento, índices gigantes, varreduras longas. Um documento por (série, janela) agrupa: 1 doc por sensor por hora, com um array de { t, v } e agregados pré-calculados (min, max, soma, n). Escolha a janela pela consulta dominante: se o dashboard mostra por minuto, bucket de 1 h; se mostra por dia, bucket de 1 dia.

💡 Coleções de série temporal nativas

MongoDB (Time Series Collections), TimescaleDB (hypertables) e InfluxDB fazem o bucketing e a compressão por você — você insere e consulta "linha a linha". Use-as por padrão; o bucket manual continua importante para entender o custo e para bancos sem esse recurso (ou para eventos que não são métricas puras).

8.2 Rollups e retenção

8.3 Event sourcing e modelos append-only

Em vez de guardar o estado atual, guarde a sequência imutável de eventos (PedidoCriado, ItemAdicionado, PedidoPago); o estado é a soma dos eventos. Implicações de modelagem:

⚠️ Event sourcing não é grátis

Ganha auditoria perfeita e "viagem no tempo"; paga com complexidade alta, versionamento eterno de eventos, necessidade de snapshots e de projeções. Adote para domínios onde o histórico de como se chegou ao estado tem valor de negócio (financeiro, jurídico, logística) — não como padrão para todo CRUD.

💼 Mercado de trabalho

Em engenharia de dados e backend de escala, espera-se saber: bucketing e por que ele existe; retenção em camadas com descarte por partição; e o suficiente de event sourcing/CQRS para conectar "read model desnormalizado" com "os padrões de modelagem NoSQL". "O dado bruto tem TTL de 14 dias, o rollup horário vive 1 ano, e o dashboard lê do rollup" é uma resposta concreta que passa.

✏️ Exercício 8 — Camadas de um produto de métricas

Um SaaS de observabilidade recebe ~50 mil métricas/segundo. Os clientes veem: gráficos ao vivo (último 1 h, resolução de segundos), gráficos históricos (até 13 meses, resolução de horas), e um relatório anual (resolução diária). Descreva o modelo de camadas, chaves e retenção.

Gabarito: Camada bruta: coleção/tabela de série temporal, chave (tenantId, metricId, dia) (coluna larga) ou time series nativa, resolução original, TTL 48–72 h (cobre o "ao vivo" com folga). Rollup horário: job/stream agrega o bruto em { tenantId, metricId, hora, min, max, avg, p95, count }, chave (tenantId, metricId, ano_mes), retenção 13 meses — serve os gráficos históricos. Rollup diário: agrega o horário, chave (tenantId, metricId) clusterizada por dia, retenção indefinida — serve o relatório anual. Descarte por partição (drop de ano_mes antigo) no rollup horário; TTL no bruto. O rollup precisa ser reprocessável por janela para tolerar métricas atrasadas.

MÓDULO 09 · MUITO AVANÇADO

Evolução de schema e migração

Objetivo: mudar a forma dos dados em coleções de bilhões de documentos, em produção, sem downtime e sem corromper — e governar o schema num time.

9.1 "Schemaless" é mentira útil

O esquema não desaparece quando o banco não o impõe — ele migra para o código da aplicação, que passa a ser o único guardião da forma dos dados. Sem disciplina, uma coleção acumula 5 formatos diferentes do mesmo documento ao longo de anos, e cada leitura vira um campo minado de if (doc.x) ... else if (doc.y).

9.2 O padrão expand / contract (paralelo ao "parallel change")

  1. Expand: a aplicação passa a escrever o formato novo e a ler os dois (novo e antigo). Nenhum dado foi tocado ainda; deploy reversível.
  2. Migrate: backfill converte os documentos antigos em lotes (ver 9.3). Novas escritas já nascem no formato novo.
  3. Contract: quando 100% está migrado e estável, remova o código que lê o formato antigo e, opcionalmente, ligue a validação de schema.

Cada passo é um deploy independente e reversível. Nunca "big bang".

9.3 Backfill que não derruba o banco

// esqueleto de backfill retomável
let ultimoId = carregarCheckpoint();
while (true) {
  const lote = await col.find({ _id: { $gt: ultimoId }, schemaVersion: { $lt: 2 } })
                        .sort({ _id: 1 }).limit(2000).toArray();
  if (lote.length === 0) break;
  for (const d of lote) await col.updateOne({ _id: d._id }, { $set: converter(d) });
  ultimoId = lote[lote.length - 1]._id;
  salvarCheckpoint(ultimoId);
  await sleep(50);
}

9.4 Validação e governança

9.5 Modelagem para IA: metadados que o RAG precisa tema em alta

Guardar embeddings num campo é o começo. O que faz o retrieval funcionar é a modelagem dos metadados: tenantId e ACLs para filtro de segurança antes da busca vetorial; fonte, página, secao para citação; dataDoc e versao para recência; chunkId/parentId para reconstruir o contexto ao redor do trecho. Um chunk é um documento; o design dele determina a qualidade da resposta. Detalhes na apostila de RAG.

💼 Mercado de trabalho

"Como você migraria o schema de uma coleção de 2 bilhões de documentos sem downtime?" é pergunta de sênior/staff. A resposta completa: expand/contract + código tolerante a N e N-1 + schemaVersion + backfill paginado, retomável e com throttle + validação ligada só no fim. Quem responde "roda um script de update" não passa.

✏️ Exercício 9 — Planeje a migração

Uma coleção usuarios (800 milhões de docs) tem nome como string única ("Ana Maria Souza"). Produto quer nomeCompleto, primeiroNome e sobrenome separados, para personalização de e-mail. O sistema está no ar 24/7. Descreva o plano completo.

Gabarito: (1) Expand — deploy da app que, ao gravar um usuário, escreve os três campos novos e mantém nome; na leitura, usa primeiroNome ?? nome.split(" ")[0]. Reversível. (2) Backfill — job paginado por _id, lotes de ~2.000, filtro { primeiroNome: { $exists: false } }, derivando os campos de nome, com schemaVersion: 2, checkpoint do último _id, sleep entre lotes e monitor de replication lag. Rodar dias/semanas conforme a folga do cluster. (3) Contract — quando { primeiroNome: { $exists: false } } zerar e estabilizar, remover o fallback de leitura; opcionalmente ligar $jsonSchema exigindo os campos novos com validationLevel: "moderate". Manter nome por mais um ciclo (ou derivá-lo de nomeCompleto) até ter certeza de que nada mais o lê. Risco tratado: nomes com uma palavra só, partículas ("de", "da"), nomes compostos — a função converter deve ter regra e um relatório dos casos ambíguos para revisão.

MÓDULO 10 · CARREIRA

Mercado de trabalho: roadmap, portfólio e entrevistas

Objetivo: usar a modelagem como diferencial — é a habilidade NoSQL que mais separa níveis e que menos gente pratica de forma deliberada.

10.1 Onde a modelagem aparece nas vagas

PapelO que cobram
Backend pleno/sênior"modele X em MongoDB/Dynamo" com raciocínio de padrões de acesso; embedding vs referencing justificado; migração sem downtime
Engenharia de Dadosmodelagem query-first em coluna larga; bucketing e retenção; read models de CQRS; schema registry
System Design (qualquer sênior+)tabela de padrões de acesso no quadro; escolha de partition/shard key; poliglota justificada
IA/ML Engineeringmodelagem de chunks e metadados para RAG; KG para GraphRAG; store de estado de agentes

10.2 Roadmap de estudo (4 semanas)

10.3 Banco de perguntas (com a resposta que aprova)

Pleno — "Como você começa a modelar algo em NoSQL?"

"Levantando os padrões de acesso antes de qualquer schema: cada tela, endpoint, job e integração vira uma linha com chave de entrada, retorno, frequência e consistência exigida. Só depois projeto as estruturas para que cada padrão de alta frequência seja uma operação por chave, sem JOIN nem varredura, aceitando duplicação de dado histórico e de exibição." Mencionar que isso é o oposto do processo relacional (dados primeiro) fecha a resposta.

Pleno — "Quando duplicar dados é aceitável e quando é perigoso?"

Aceitável quando o dado é imutável/histórico (preço na hora da compra) ou uma projeção de exibição estável (nome para a lista). Perigoso quando é mutável e compartilhado por muitos: aí a atualização vira fan-out de milhões de escritas. Saídas: não duplicar o mutável (resolver na leitura, com cache/batch) ou duplicar e propagar de forma assíncrona, tolerando inconsistência temporária — nunca para permissões.

Sênior — "Explique single-table design e quando NÃO usar."

Várias entidades numa tabela DynamoDB, com PK/SK genéricas preenchidas por prefixo (key overloading), de modo que entidades relacionadas compartilhem a PK e uma Query traga o agregado inteiro (o JOIN pré-computado pela colocação física). GSIs esparsos atendem outros padrões e servem de filas. Não usar quando: os padrões de acesso são voláteis (cada novo exige GSI/backfill), o time não tem experiência em Dynamo, o app é pequeno, ou há muita necessidade de consulta ad-hoc/analytics — nesses casos, várias tabelas ou outro banco são a escolha madura.

Sênior — "Modele o backend de mensagens de um chat."

Coluna larga: tabela mensagens_por_canal com PRIMARY KEY ((canal_id, bucket_dia), msg_id) e CLUSTERING ORDER BY (msg_id DESC) — leitura de "últimas N do canal" é uma partição; o bucket_dia limita o tamanho da partição e distribui a escrita. Se há "minhas mensagens", uma segunda tabela mensagens_por_autor alimentada pela mesma escrita. Presença/últimos vistos em Redis com TTL. Busca de mensagens em Elasticsearch via CDC. Não deletar linha a linha (tombstones) — TTL ou drop de bucket. É o design do Discord.

Sênior — "Como migrar o schema de uma coleção gigante sem downtime?"

Expand/contract: (1) app escreve o formato novo e lê os dois; (2) backfill paginado por _id, em lotes, retomável, idempotente (schemaVersion), com throttle e monitor de replication lag; migração preguiçosa cobre os quentes; (3) quando 100% migrado, remove a leitura do formato antigo e liga a validação. Cada passo é deploy reversível. Nunca updateMany({}, ...) nem big bang.

10.4 Projetos de portfólio

  1. "O mesmo app, três modelagens": um domínio (ex.: reservas de restaurante) modelado em MongoDB (documento + padrões), DynamoDB (single-table) e Cassandra (tabela-por-consulta), com um README comparando padrões de acesso atendidos, custo de escrita e trade-offs. Raro e muito convincente.
  2. Migração documentada: um repo que parte de um schema "ingênuo", tem um branch com o backfill expand/contract completo (job retomável, testes, métricas antes/depois) e um post-mortem.
  3. Modelagem de KG para RAG: extrair entidades/relações de um corpus, definir ontologia e resolução de entidades, e comparar RAG puro vs GraphRAG em algumas perguntas.

10.5 Fontes

🏁 Síntese final

Uma frase carrega a apostila: o schema é uma função dos padrões de acesso. Levante as consultas primeiro; projete estruturas onde cada consulta frequente é uma operação por chave; duplique dado histórico e de exibição sem culpa; trate dado mutável compartilhado com cuidado; e evolua o schema por expand/contract, nunca por big bang. Documento, chave-valor, coluna larga e grafo mudam a mecânica — não o método.