MÓDULO 03 · CONCEITO 06 DE 8

NoSQL: quando faz sentido

Document, key-value, column-family, graph, vector. CAP aplicado, BASE vs ACID — e o caminho de volta para SQL quando o entusiasmo passa.

Tempo de leitura ~22 min Pré-requisito ACID + modelagem Próximo Replicação & sharding

O termo "NoSQL" foi cunhado em 2009 numa conferência informal em San Francisco, e por uma década carregou sentido moralista: SQL era legado, NoSQL era o futuro. A onda gerou uma pluralidade de sistemas — MongoDB, Cassandra, Redis, Neo4j, DynamoDB, e dezenas de outros — cada um com modelo de dados próprio, garantias próprias e armadilhas próprias. Quinze anos depois, a poeira assentou. NoSQL não substituiu SQL; conviveu com ele. Mais importante: o entendimento maduro hoje é que cada storage tem cabimento específico, e a habilidade sênior é saber reconhecer quando.

Este conceito é menos "tutorial de Mongo" e mais "framework de decisão". O foco é entender as quatro grandes famílias NoSQL (mais a emergente família vector), o que cada uma sacrifica em troca do que oferece, e os critérios para escolher conscientemente. Termina com o caminho de retorno: como sistemas que começaram em NoSQL frequentemente migram (parcial ou totalmente) para SQL — e o que aprender desses casos.

O contexto histórico — por que NoSQL apareceu

No fim dos anos 2000, três pressões reais empurraram a indústria para fora de SQL relacional puro. Empresas web tinham volumes de dados que rompiam a verticalização (Google, Facebook, Amazon operavam escalas que Oracle/MySQL/Postgres não escalavam horizontalmente bem). Schemas evoluíam mais rápido que migrations podiam acompanhar — produtos que iteravam diariamente brigavam com schemas rígidos. E tipos de dados específicos (séries temporais, grafos sociais, logs) eram mal-servidos por tabelas relacionais.

Os trabalhos que detonaram a onda: Bigtable (Chang et al., 2006) descreveu a abordagem column-family do Google; Dynamo (DeCandia et al., 2007) descreveu o sistema key-value altamente disponível da Amazon, e o teorema CAP (Eric Brewer, 2000; provado por Gilbert e Lynch em 2002) formalizou os trade-offs entre consistência, disponibilidade e tolerância a partições. Esses três artigos juntos fizeram pensar que o relacional teria que ser substituído. Não aconteceu — mas mudou o que se construiu em paralelo.

CAP, BASE e o que de fato significa

O teorema CAP diz: em sistema distribuído, na presença de partições de rede (P inevitável), você escolhe entre Consistência forte (C) e Disponibilidade (A). Não é os três; é dois. Em operação normal sem partição, sistemas podem oferecer ambos — o trade-off só pesa durante falha.

Sistemas SQL tradicionais tendem ao "CP": diante de partição, preferem indisponibilidade a inconsistência. NoSQLs como Cassandra e Dynamo tendem ao "AP": diante de partição, preferem responder com possível dado stale a recusar atender. A escolha depende do domínio: sistema bancário precisa CP (saldo errado é inaceitável); cache de feed social pode AP (timeline atrasada por segundos é tolerável).

BASE — Basically Available, Soft state, Eventual consistency — é o oposto deliberado de ACID. Reconhece que consistência forte tem custo, e propõe modelo onde o sistema converge para consistência ao longo do tempo, em vez de garantir instantaneamente. Em sistemas AP, BASE é o vocabulário operacional: você escreve em qualquer nó, propagação acontece em background, leitura pode ver versão antiga até convergir.

A nuance perdida: "eventually consistent" sem prazo é só "inconsistent". Sistemas BASE bem-projetados definem janelas — em condições normais, propagação em milissegundos; em falha, segundos a minutos; SLA de convergência. Sem esse cuidado, "eventual" vira hand-wave.

princípio orientador

CAP é menos sobre escolher um lado e mais sobre saber qual sua aplicação tolera. Vendas de ingressos não toleram dois usuários comprarem o mesmo lugar (CP). Tarja de "online" em rede social tolera lag de minutos (AP). Modelar o domínio inclui decidir o tipo de inconsistência aceitável.

Document — MongoDB, CouchDB, DocumentDB

Documentos são objetos JSON-like com schema implícito. Cada documento é independente: pode ter campos diferentes do vizinho. Coleções agrupam documentos relacionados. Não há joins (em formas mais puras); relacionamentos são embedados ou referenciados.

O que MongoDB e similares fazem bem:

Onde costuma falhar:

Quando MongoDB faz sentido: domínio que genuinamente é catálogo de objetos heterogêneos com poucos relacionamentos cruzados — catálogos de produto com atributos por categoria muito variados, configurações por tenant, eventos brutos antes de processamento. Em domínios relacionais reais, Postgres com JSONB cobre 90% do que MongoDB ofereceria, com transações fortes.

Key-Value — Redis, Memcached, DynamoDB

A primitiva: set(key, value), get(key). Sem schema, sem queries em valor, sem joins. Apenas lookup por chave, na ordem de microssegundos quando em memória.

Categoria menos sobre "armazenar dados" e mais sobre acelerar acesso. Redis se tornou ferramenta universal para:

DynamoDB é diferente: KV gerenciado da AWS com modelo de capacidade pré-paga, sharding automático, e disponibilidade altíssima por design. Escolha para sistemas com perfis de acesso muito conhecidos e necessidade de latência consistente em escala. Modelagem em DynamoDB é arte própria — Rick Houlihan da AWS é referência aqui.

Quando KV faz sentido: caches, sessões, rate limiting, contadores, lookups por chave conhecida, estruturas auxiliares onde latência sub-milissegundo importa. Não é substituto para banco — é complemento. Sistemas maduros usam Redis e Postgres, cada um onde brilha.

Column-family — Cassandra, ScyllaDB, HBase, BigTable

Modelo derivado do Bigtable do Google. Visualize uma estrutura esparsa onde cada linha tem chave primária composta (partition key + clustering key), e colunas podem ser adicionadas dinamicamente por linha. Linhas de uma partição ficam fisicamente juntas; queries leem partições inteiras eficientemente.

Cassandra (e seu sucessor mais rápido, ScyllaDB) brilham em:

Onde sofre:

Quando faz sentido: dados time-series em escala (métricas de IoT, logs de aplicação), feeds onde writes superam reads em ordens de magnitude, sistemas distribuídos geograficamente onde disponibilidade trumps consistência forte. Para sistema de negócio comum, Cassandra é overkill cuja complexidade não compensa.

Graph — Neo4j, Amazon Neptune, ArangoDB

Modelo onde nós (entidades) e arestas (relacionamentos) são cidadãos de primeira classe. Queries percorrem o grafo naturalmente — "amigos de amigos do João que moram em São Paulo" é uma travessia direta, não um auto-join encadeado.

Onde graph databases ganham claramente do relacional: queries de profundidade variável (5+ saltos) sobre conexões. Com SQL, cada salto é um JOIN; cinco saltos viram queries impossíveis de otimizar. Graph DB executa em tempo proporcional ao subgrafo explorado.

Casos canônicos:

O desafio operacional: graph databases são, em geral, menos maduros que Postgres em durabilidade, ferramentas, comunidade. Postgres com extensão Apache AGE oferece grafos sobre Postgres, e é boa opção quando o sistema é majoritariamente relacional com algumas queries de grafo.

Vector — pgvector, Pinecone, Weaviate, Qdrant

A categoria emergente. Vetores (arrays de números, tipicamente 384-1536 dimensões) representam embeddings semânticos produzidos por modelos como text-embedding-3, BGE, E5. Vector DBs especializam-se em busca por similaridade — "documentos mais similares a este texto", "imagens mais parecidas a essa".

Algoritmos centrais: HNSW (Hierarchical Navigable Small World), IVFFlat, ScaNN. Todos balanceiam precisão vs latência via aproximação — busca exata em milhões de vetores não escala; busca aproximada (ANN) entrega 95%+ de precisão em milissegundos.

A questão de 2024-2026: Postgres com pgvector é suficiente para o seu caso, ou você precisa de DB dedicado? Para volumes até dezenas de milhões de vetores e necessidade de filtragem combinada (vetor + metadados SQL), pgvector é excelente. Para bilhões de vetores ou necessidade de indexação especializada (filtros multi-vetor, multi-tenancy em escala), DBs dedicados ainda ganham.

Padrão emergente sênior: RAG sobre Postgres — embeddings em pgvector, dado relacional na mesma instância, JOINs naturais entre semântica e SQL. Reduz infra, mantém ACID sobre o conjunto.

Polyglot persistence — usar vários, com critério

Sistemas maduros raramente usam só um banco. Padrão típico:

A regra é: cada storage adicionado é complexidade adicional. Backups separados, monitoramento separado, runbooks separados, expertise no time. Adicione apenas quando o ganho do storage especializado supera o custo operacional — e quando o time tem capacidade para cuidar de todos.

Sistemas pequenos a médios fazem bem com Postgres-only por muito tempo. "Just use Postgres for everything" virou provocação justamente porque é defensável: full-text search via tsvector, filas via SKIP LOCKED, cache via tabelas e UNLOGGED, vetores via pgvector, time-series via partitioning ou TimescaleDB. Postgres come um terço da complexidade que vinha justificada por usar quatro bancos.

armadilha de adoção

Adotar storage nova porque uma feature foi entusiasticamente adotada por uma big tech. Netflix usa Cassandra; isso não significa que sua aplicação com 10k usuários deve. As escolhas de empresas com bilhões de eventos por dia são quase nunca diretamente aplicáveis a sistemas de outra escala. Olhe o seu volume, sua complexidade real, sua equipe — não o blog post.

O caminho de volta — quando NoSQL volta a SQL

Casos públicos de migração reversa formaram um sub-gênero da indústria nos últimos anos:

Padrões comuns nessas histórias:

  1. Domínio se mostrou mais relacional do que parecia.
  2. Operação de NoSQL exigiu times maiores que SQL equivalente.
  3. Garantias de consistência viraram requisito (compliance, audit).
  4. Custo total (infra + pessoas) ficou maior que SQL bem-tunado.

A lição não é "NoSQL é ruim". É: escolha consciente bate moda. Hype te faz adotar; entender o trade-off te permite ficar.

Acesso aos diferentes storages — visão geral

C# — múltiplos storages
// Postgres (relacional + JSONB + pgvector)
await using var conn = await npgsqlDataSource.OpenConnectionAsync();
await conn.ExecuteAsync(
    "INSERT INTO docs(id, conteudo, embedding) VALUES (@id, @c, @v)",
    new { id, c = conteudo, v = new Vector(embedding) });

// Redis (StackExchange.Redis)
var db = redis.GetDatabase();
await db.StringSetAsync($"sess:{token}", json, TimeSpan.FromMinutes(30));
await db.SortedSetAddAsync("ranking:diario", userId, score);

// MongoDB (oficial driver)
var pedidos = mongoDb.GetCollection<Pedido>("pedidos");
await pedidos.InsertOneAsync(new Pedido {
    ClienteId = id, Itens = items, CriadoEm = DateTime.UtcNow });

// Neo4j (driver oficial)
await using var session = neo4j.AsyncSession();
await session.RunAsync(
    "MATCH (u:Usuario {id: $id})-[:SEGUE*1..3]->(amigo) RETURN amigo",
    new { id });

Cada storage tem driver oficial bem-mantido em .NET. Padrão sênior: encapsular cada acesso em repositório distinto e não deixar tipos de driver vazarem para o domínio.

Python — múltiplos storages
# Postgres + pgvector via SQLAlchemy
from sqlalchemy import insert
from pgvector.sqlalchemy import Vector
session.execute(insert(Doc), {"id": id, "conteudo": c, "embedding": v})

# Redis (redis-py async)
async with redis_client.pipeline() as pipe:
    await pipe.set(f"sess:{token}", json_data, ex=1800).execute()
    await pipe.zadd("ranking:diario", {user_id: score}).execute()

# MongoDB (motor async)
await motor_db.pedidos.insert_one({
    "cliente_id": id, "itens": items, "criado_em": datetime.utcnow()
})

# Neo4j
async with neo4j_driver.session() as session:
    result = await session.run(
        "MATCH (u:Usuario {id: $id})-[:SEGUE*1..3]->(a) RETURN a",
        id=user_id)

Python tem drivers async maduros para todas as categorias. Em microsserviço, frequentemente faz sentido instâncias separadas (cada uma com seu storage especializado), em vez de um monolito poliglota.

Go — múltiplos storages
// Postgres (pgx)
_, err := pool.Exec(ctx,
    `INSERT INTO docs(id, conteudo, embedding) VALUES ($1, $2, $3)`,
    id, conteudo, pgvector.NewVector(embedding))

// Redis (go-redis/v9)
err = rdb.Set(ctx, "sess:"+token, json, 30*time.Minute).Err()
rdb.ZAdd(ctx, "ranking:diario", redis.Z{Score: score, Member: userID})

// MongoDB (mongo-go-driver)
_, err = mongoDB.Collection("pedidos").InsertOne(ctx, bson.M{
    "cliente_id": id, "itens": items, "criado_em": time.Now(),
})

// Neo4j (driver oficial)
result, err := neo4j.ExecuteRead(ctx, session,
    func(tx neo4j.ManagedTransaction) (any, error) {
        return tx.Run(ctx,
            "MATCH (u:Usuario {id: $id})-[:SEGUE*1..3]->(a) RETURN a",
            map[string]any{"id": userID})
    })

Go tem ecossistema maduro para os principais storages. A tendência é wrappers leves; clean architecture valoriza interfaces que abstraem o driver — útil para testes e troca de implementação.

Framework de decisão — perguntas a fazer

Quando alguém propõe um storage novo no sistema, oito perguntas:

  1. Qual problema concreto não resolve com o storage atual? Se a resposta é "nada específico, mas seria moderno", já está respondida.
  2. Qual SLA de consistência o domínio exige? Algum dado precisa ser perfeitamente consistente? Pode ficar eventual?
  3. Qual o volume real? Linhas, escrita por segundo, leitura por segundo, em produção. Não estimativa, números medidos. Postgres sobre hardware decente cobre uma faixa muito maior do que reflexo de "Postgres não escala" sugere.
  4. Quais queries / padrões de acesso? Lookups por chave? Range scans? Joins? Agregações? Filtragem de vetor?
  5. Qual a frequência de mudança de schema? Se o modelo muda toda semana, schema-less ajuda — mas considere se a mudança é sintoma de modelagem mal-feita.
  6. Quem opera? Time tem expertise? Ferramentas de monitoramento? Backup, DR, upgrade — quem cuida?
  7. Qual o custo total? Licença/managed cost + infra + pessoas + tempo de migração. NoSQL gerenciado (DynamoDB, Atlas) reduz dor operacional mas explode custo.
  8. Existe um caminho de saída? Como migrar de volta se virar dívida? "Lock-in inicial é dívida latente".

Como praticar

  1. Escolha um caso real do seu sistema e modele ele em três storages distintos (Postgres, MongoDB, Cassandra, por exemplo). Implemente as 3-4 queries críticas em cada. Compare verbosity, performance, complexidade operacional.
  2. Monte um stack poliglota mínimo com Postgres + Redis. Implemente cache-aside e rate limiting. Meça impacto com e sem cache em latência P50, P95, P99 e em throughput. Quase todo sistema sênior usa esse par — vivê-lo é formativo.
  3. Replique um caso clássico de migração reversa. Modele um caso simples primeiro em MongoDB; depois migre os dados para Postgres (talvez mantendo JSONB para os campos flexíveis). Documente o que foi mais simples, o que foi mais difícil, o que melhorou na nova versão.

Referências para aprofundar

  1. livro Designing Data-Intensive Applications — Martin Kleppmann (2017). Cap. 2 e 3 cobrem modelos de dados (relacional, documento, grafo) e storage engines com profundidade incomum. Referência obrigatória para entender NoSQL com calma.
  2. livro NoSQL Distilled — Pramod Sadalage & Martin Fowler (2012). Mapa conciso das categorias NoSQL. Datado em alguns detalhes; conceitos centrais válidos.
  3. livro The DynamoDB Book — Alex DeBrie (2020). A referência de modelagem em DynamoDB. Mostra que single-table design é arte, não sintaxe.
  4. livro Database Internals — Alex Petrov (2019). Cap. sobre LSM-trees e B-trees mostra por que Cassandra/RocksDB têm perfis de write diferentes de Postgres. Forma intuição estrutural.
  5. paper Bigtable: A Distributed Storage System for Structured Data — Chang et al. (2006). research.google — paper original do Bigtable. Influenciou Cassandra, HBase e similares.
  6. paper Dynamo: Amazon's Highly Available Key-value Store — DeCandia et al. (2007). Paper que originou DynamoDB e influenciou Cassandra, Riak. Discute consistent hashing, vector clocks, quorum.
  7. artigo Just use Postgres for everything — Stephan Schmidt (2023). amazingcto.com — provocação fundamentada com lista de casos onde Postgres substitui storages especializados.
  8. artigo Why we moved from MongoDB to Postgres — vários posts agregados. É um sub-gênero. Procure casos de Etsy, Crunchbase, Foursquare, Disqus. Padrões comuns valem mais que um único caso.
  9. artigo Don't use Cassandra for this — DataStax engineering. Mesmo a DataStax (vendor) tem documentação sobre quando NÃO usar. Ler material vendor crítico é educativo.
  10. docs pgvector. github.com/pgvector/pgvector — extensão Postgres para vetores. Documentação direta com exemplos de HNSW e IVFFlat.
  11. vídeo Advanced Design Patterns for DynamoDB — Rick Houlihan, AWS re:Invent. YouTube. A introdução canônica a single-table design. 50 minutos densos, com exemplos visuais.
  12. vídeo SQL vs NoSQL: When to use what — Hussein Nasser. YouTube. Tratamento didático com casos práticos e whiteboarding.