Trilha 12 · Bancos de dados

Escala e NoSQL

Tudo o que você viu até aqui — transações, JOINs, integridade — assume um banco numa máquina só. Quando os dados não cabem mais nela, cada uma dessas garantias vira uma negociação. Esta lição é sobre o que exatamente se perde ao distribuir, o que se ganha em troca, e por que a resposta certa quase sempre é "ainda não".

① Intuição

Distribuir é trocar garantias por capacidade

Numa máquina só, "o saldo é 1000" é um fato simples: existe um lugar onde esse número mora. Em cinco máquinas, a frase precisa de qualificação. Saldo segundo qual servidor? E se dois receberem escritas diferentes ao mesmo tempo? E se um deles não estiver falando com os outros neste instante?

Nada disso é falha de engenharia — é consequência de a informação levar tempo para viajar. Um sistema distribuído não tem um "agora" compartilhado. Toda tecnologia distribuída é, no fundo, uma escolha específica sobre como conviver com essa ausência.

Antes de distribuir, esgote o simples. A maioria absoluta dos "problemas de escala" é um índice faltando, uma consulta N+1 ou a ausência de cache. Uma única máquina moderna aguenta dezenas de milhares de transações por segundo e terabytes de dados, mantendo todas as garantias ACID. Sharding prematuro é uma das formas mais caras de complexidade acidental: você paga o custo permanente e ainda não tinha o problema.
② Visualização interativa

Replicação, sharding e a escolha do CAP

Na primeira aba, escreva no primário e veja o lag de replicação criar leituras desatualizadas. Na segunda, compare sharding por hash e por faixa — marque a caixa de registros recentes para ver o ponto quente aparecer. Na terceira, particione a rede e escolha entre consistência e disponibilidade.

Replicação é ter cópias dos mesmos dados em várias máquinas — para sobreviver à queda de uma delas e para dividir a carga de leitura. A pergunta difícil é: o commit deve esperar as cópias?

PRIMÁRIO
1000
aceita escritas
réplica 1
1000
só leitura
réplica 2
1000
só leitura
O primário recebe as escritas; as réplicas as copiam e atendem leituras.
③ Explicação técnica

A ordem certa de escalar

# A ordem certa de escalar um banco — e quase ninguém precisa
# passar do passo 4.
#
#  1. ÍNDICES E CONSULTAS       ganho de 10x a 1000x, custo ~zero
#     A maioria dos "problemas de escala" é uma consulta ruim.
#
#  2. CACHE                     Redis na frente das leituras quentes
#     Barato, mas traz o problema clássico: invalidação.
#
#  3. ESCALA VERTICAL           mais RAM e CPU numa máquina só
#     Subestimado. Uma máquina moderna com 1 TB de RAM aguenta
#     muito mais do que a maioria dos sistemas jamais vai exigir,
#     e mantém tudo simples e transacional.
#
#  4. RÉPLICAS DE LEITURA       cópias que atendem SELECTs
#     Resolve leitura pesada. Não resolve escrita, e traz o lag.
#
#  5. SHARDING                  particionar os dados em N servidores
#     A última opção, e a mais cara. JOINs entre shards deixam de
#     existir, transações entre shards viram um problema em aberto,
#     e reparticionar depois é uma migração dolorosa.
#
# Regra: só suba um degrau depois de MEDIR que o anterior acabou.

Sharding: a decisão irreversível

# Escolher a chave de partição é a decisão mais difícil de reverter
# em todo o sistema. Ela define o que fica junto — e o que nunca mais
# poderá ser consultado numa operação só.

# POR HASH — distribuição uniforme, sem ponto quente
shard = hash(usuario_id) % N
#   ✓ carga equilibrada mesmo com ids sequenciais
#   ✗ chaves vizinhas ficam separadas: consulta por intervalo
#     precisa perguntar a TODOS os shards (scatter-gather)
#   ✗ com "% N" puro, mudar N remaneja quase tudo

# CONSISTENT HASHING — resolve o problema do "% N"
#   Servidores e chaves são posicionados num anel de 0 a 2³²;
#   cada chave pertence ao primeiro servidor em sentido horário.
#   Adicionar um servidor move só ~1/N das chaves, não quase todas.
#   Base do Cassandra, do DynamoDB e do roteamento de CDNs.

# POR FAIXA — vizinhos ficam juntos
shard = 0 if data < "2024-01" else 1
#   ✓ consulta por intervalo toca um shard só
#   ✗ PONTO QUENTE: com ids ou timestamps crescentes, todo o
#     tráfego novo vai para o último shard e os outros ficam ociosos

# O que se PERDE ao shardear, em qualquer estratégia:
#   . JOIN entre shards (ou é feito na aplicação, ou não existe)
#   . transações ACID atravessando shards
#   . UNIQUE global (o banco só garante unicidade dentro do shard)
#   . AUTO_INCREMENT global (daí UUIDs, ULIDs, Snowflake IDs)
O teorema CAP, sem o folclore. A formulação popular — "escolha 2 de 3 entre Consistência, Disponibilidade e tolerância a Partição" — é enganosa, porque partição não é uma opção: redes falham, ponto. A leitura correta é condicional: quando houver uma partição, você escolhe entre recusar operações (CP) ou aceitá-las e divergir (AP). Fora da partição — ou seja, quase sempre — você tem as duas. Uma extensão útil é o PACELC: se há Partição, escolha entre A e C; senão (Else), escolha entre Latência e Consistência. É essa segunda metade que descreve o dia a dia de um sistema distribuído.

As famílias NoSQL

-- "NoSQL" não é uma tecnologia: é um guarda-chuva para bancos que
-- abriram mão de algo do modelo relacional para ganhar outra coisa.

-- CHAVE-VALOR — Redis, DynamoDB, Memcached
--   get(chave) / put(chave, valor). Só isso.
--   Ganha: latência sub-milissegundo, escala trivial
--   Perde: só se busca pela chave. Sem consulta por conteúdo.
--   Use para: cache, sessões, rate limiting, filas, contadores

-- DOCUMENTOS — MongoDB, CouchDB, Firestore
--   Guarda JSON aninhado; o documento inteiro é lido de uma vez.
--   Ganha: esquema flexível, sem JOIN para dados aninhados
--   Perde: consistência entre documentos, e o esquema não some —
--          ele só migra do banco para o seu código de leitura
--   Use para: catálogos heterogêneos, documentos naturalmente
--             autocontidos, prototipagem

-- COLUNAR LARGO — Cassandra, HBase, ScyllaDB
--   Você modela a partir das CONSULTAS, não das entidades, e
--   duplica dados de propósito (uma tabela por padrão de acesso).
--   Ganha: escrita massiva, escala linear, sem ponto único de falha
--   Perde: consultas ad-hoc — se não estava no modelo, não dá
--   Use para: séries temporais, telemetria, feeds, logs em escala

-- GRAFOS — Neo4j, Neptune
--   Relacionamentos são cidadãos de primeira classe.
--   Ganha: percorrer conexões profundas sem N JOINs
--   Use para: redes sociais, detecção de fraude, recomendação

-- ANALÍTICO / COLUNAR — ClickHouse, BigQuery, Snowflake, DuckDB
--   Guarda por COLUNA, não por linha: ler uma coluna de 1 bilhão
--   de linhas não toca nas outras 40, e comprime muito melhor.
--   Perde: escrita e atualização linha a linha
--   Use para: BI, agregações sobre volumes enormes

O pêndulo de volta

-- O movimento dos últimos anos: o pêndulo voltou.
--
-- 2010: "bancos relacionais não escalam, precisamos de NoSQL"
-- 2025: os relacionais aprenderam quase tudo que faltava —
--
--   . JSONB no PostgreSQL, com índices GIN: documentos COM
--     transações e possibilidade de JOIN quando precisar
--   . particionamento nativo de tabelas
--   . replicação lógica e réplicas de leitura simples de operar
--   . NewSQL — CockroachDB, TiDB, Spanner, Yugabyte: SQL e ACID
--     distribuídos de verdade, em vários datacenters
--   . SQLite e DuckDB embutidos, cobrindo dois extremos

CREATE TABLE eventos (
    id      BIGSERIAL PRIMARY KEY,
    tipo    TEXT NOT NULL,
    dados   JSONB                       -- flexível onde precisa ser
);
CREATE INDEX ON eventos USING GIN (dados);
SELECT * FROM eventos WHERE dados @> '{"user": 42}';

-- O conselho que envelheceu bem:
-- comece com PostgreSQL. Ele resolve o problema de 95% dos
-- sistemas, e você só vai saber qual trade-off precisa aceitar
-- depois de ter carga real. Escolher um banco distribuído antes
-- de ter o problema é pagar toda a complexidade sem o benefício.
④ Projeto para programar

Distribuindo de verdade

Mini projeto: implemente consistent hashing em Python. Um anel com N servidores virtuais, uma função que mapeia chave → servidor, e uma medição: distribua 100 mil chaves entre 4 servidores, adicione o quinto e conte quantas mudaram de lugar. Compare com hash(k) % N puro. A diferença — algo como 20% contra 80% — é a razão de existir da técnica. Depois adicione nós virtuais (cada servidor ocupando 150 pontos do anel) e veja a distribuição ficar visivelmente mais uniforme.

Projeto principal: suba um PostgreSQL com uma réplica de leitura (dois containers Docker com replicação em streaming, ou o serviço gerenciado da sua nuvem). Escreva um script que insere no primário e imediatamente lê da réplica, medindo com que frequência lê dado velho. Depois aumente a carga de escrita e observe o lag crescer. Por fim, implemente read-your-writes: direcione ao primário as leituras de um usuário nos segundos seguintes a uma escrita dele.

Desafio extra: modele o mesmo domínio — digamos, um feed de rede social — em três bancos: PostgreSQL normalizado, MongoDB com documentos e Cassandra com tabelas por padrão de consulta. Para cada um, escreva as cinco consultas mais comuns do produto e anote o que ficou fácil e o que ficou impossível. O exercício não tem vencedor: o objetivo é sentir na prática que a escolha do banco é a escolha de quais perguntas serão baratas.

⑤ Exercícios rápidos

Teste sua intuição

O que o teorema CAP realmente afirma?
Qual é a diferença entre replicação e sharding?
Qual é o risco de shardear por faixa de valores quando as chaves são sequenciais (ids crescentes, timestamps)?
Qual é a consequência de usar replicação assíncrona?
⑥ Aplicações no mundo real

Onde você encontra isso

🛒

O carrinho da Amazon e o Dynamo

O artigo do Dynamo (2007) é a origem prática da consistência eventual. A decisão de projeto foi explícita: para um carrinho de compras, dizer "estamos fora do ar" custa uma venda, enquanto aceitar duas versões do carrinho e mesclá-las depois custa, no pior caso, um item reaparecendo. Escolheram AP conscientemente — e essa escolha só fazia sentido para aquele caso de uso. O checkout e o pagamento continuaram consistentes.

⏱️

Spanner e o relógio como infraestrutura

O Spanner, do Google, oferece transações ACID serializáveis globalmente distribuídas — algo tido como impraticável. O truque é a TrueTime: relógios atômicos e receptores de GPS em cada datacenter que limitam a incerteza temporal a poucos milissegundos. Transações esperam esse intervalo antes de confirmar, garantindo ordem global. É o caso raro em que a solução para um problema de software foi comprar hardware.

🧩

CRDTs e a colaboração em tempo real

Google Docs, Figma e Notion precisam aceitar edições simultâneas de várias pessoas, inclusive offline, e convergir sem conflito. A base são os CRDTs (conflict-free replicated data types): estruturas cujas operações são projetadas para que a ordem de aplicação não importe — o resultado converge sozinho. É a resposta de "AP feito direito": em vez de last-write-wins descartando trabalho, mesclagem que preserva a intenção de todo mundo.

← Anterior: Transações e ACID Concluir trilha: Bancos de dados →