Banco de Dados • 6 min de leitura

Bancos de dados NoSQL: quando sair do modelo relacional faz sentido

Os bancos de dados NoSQL surgiram como uma alternativa aos tradicionais sistemas gerenciados por banco de dados, mais conhecidos como RDBMS (Relational Database Management Systems), que enfrentam dificuldades em lidar com as crescentes quantidades de dados geradas pela digitalização e conectividade das sociedades contemporâneas. Com a expansão da internet e do uso de aplicações web, as necessidades dos usuários mudaram significativamente, exigindo flexibilidade e escalabilidade nos sistemas de armazenamento.

Durante décadas, bancos relacionais foram a resposta padrão para qualquer problema de persistência. Tabelas, chaves estrangeiras, joins, transações ACID — um modelo consolidado que funciona bem para uma classe enorme de problemas. O que mudou não foi o modelo em si, mas o tipo de dado que as aplicações modernas precisam armazenar.

Redes sociais com grafos de relacionamento que chegam a bilhões de nós. Catálogos de e-commerce onde cada produto tem atributos completamente diferentes do próximo. Logs de eventos gerados a dezenas de milhares por segundo. Sessões de usuário que precisam ser lidas em microssegundos. Para esses cenários, forçar os dados em linhas e colunas não é só ineficiente — é um atrito constante entre o problema e a solução.

NoSQL não é um banco de dados. É uma família de abordagens de armazenamento que trocam algumas garantias dos relacionais — em geral consistência imediata e flexibilidade de query — por escalabilidade horizontal, esquemas flexíveis e performance em padrões específicos de acesso. A escolha entre eles começa entendendo quais dessas trocas fazem sentido para o seu caso.

Os quatro modelos principais têm características bem distintas. Bancos chave-valor como Redis e DynamoDB são essencialmente tabelas hash distribuídas: acesso O(1) por chave, sem query complexa, ideais para cache, sessões e filas. Bancos documentais como MongoDB e CouchDB armazenam JSON (ou equivalentes) com esquema livre — cada documento pode ter campos diferentes, e queries podem navegar dentro da estrutura. Bancos colunares como Cassandra e HBase organizam dados por coluna em vez de linha, o que os torna extremamente eficientes para leituras analíticas em grandes volumes. Bancos de grafos como Neo4j tratam relacionamentos como cidadãos de primeira classe, tornando travessias de conexões ordens de grandeza mais rápidas do que joins encadeados.

A escolha errada aqui custa caro. Usar MongoDB onde você precisava de Redis adiciona latência desnecessária. Usar Cassandra onde você precisava de Neo4j transforma queries simples em problemas de modelagem.

Para ilustrar o modelo documental na prática, um e-commerce em MongoDB:

// Produto com atributos heterogêneos — sem schema fixo
await db.collection('produtos').insertOne({
  nome: 'Camisa Polo',
  preco: 49.90,
  estoque: 100,
  atributos: { cor: 'preta', tamanhos: ['P', 'M', 'G'], material: 'algodão' }
});

// Índice no campo mais consultado
await db.collection('produtos').createIndex({ nome: 'text' });

// Query com projeção — só os campos necessários
const produto = await db.collection('produtos').findOne(
  { nome: 'Camisa Polo' },
  { projection: { nome: 1, preco: 1, estoque: 1 } }
);

O mesmo padrão em Python com Motor (driver async para MongoDB):

import motor.motor_asyncio

client = motor.motor_asyncio.AsyncIOMotorClient("mongodb://localhost:27017")
db = client.loja

async def buscar_produto(nome: str) -> dict | None:
    return await db.produtos.find_one(
        {"nome": nome},
        {"_id": 0, "nome": 1, "preco": 1, "estoque": 1}
    )

async def atualizar_estoque(nome: str, quantidade: int) -> None:
    await db.produtos.update_one(
        {"nome": nome},
        {"$inc": {"estoque": -quantidade}}
    )

E em Rust com o driver oficial:

use mongodb::{Client, bson::doc};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Produto {
    nome: String,
    preco: f64,
    estoque: i32,
}

async fn buscar_produto(db: &mongodb::Database, nome: &str) -> Option<Produto> {
    db.collection::<Produto>("produtos")
        .find_one(doc! { "nome": nome }, None)
        .await
        .ok()
        .flatten()
}

async fn atualizar_estoque(db: &mongodb::Database, nome: &str, quantidade: i32) {
    db.collection::<Produto>("produtos")
        .update_one(
            doc! { "nome": nome },
            doc! { "$inc": { "estoque": -quantidade } },
            None,
        )
        .await
        .ok();
}

Independente da linguagem, alguns padrões aparecem consistentemente em implementações que funcionam bem em produção.

Índices precisam refletir os padrões reais de query — não os campos que parecem importantes no papel. Um índice criado sem análise de acesso é espaço em disco e overhead de escrita sem retorno. O oposto também é verdade: ausência de índice em um campo consultado frequentemente transforma leituras rápidas em full scans.

Documentos grandes são tentadores no modelo documental — "tudo junto em um documento" parece conveniente até o documento chegar a megabytes. Nesse ponto, cada leitura carrega mais dado do que o necessário, e atualizações parciais ficam caras. Arquivos binários pertencem a object storage (S3, GCS); o documento guarda a referência.

A distribuição das chaves importa mais do que parece. Em clusters Cassandra ou DynamoDB, chaves concentradas em poucos valores criam hotspots — alguns nós sobrecarregados enquanto outros ficam ociosos. Modelar a chave de partição para distribuição uniforme é parte do design, não um detalhe de operação.

Transações complexas são o ponto onde NoSQL mais frequentemente decepciona quem migra de relacionais sem atenção. MongoDB tem transações multi-documento desde a versão 4.0, mas com custo de performance. Cassandra tem consistência eventual por padrão. Entender o modelo de consistência do banco escolhido — e desenhar a aplicação em torno dele, não contra ele — é o que separa implementações que escalam das que criam problemas silenciosos.

Segurança merece atenção específica. Bancos NoSQL, especialmente MongoDB em configurações padrão mais antigas, chegaram a ter instâncias abertas na internet sem autenticação — um vetor de ataque trivial. Queries construídas com concatenação de strings em vez de parâmetros são vulneráveis a injection mesmo em bancos documentais. O padrão é sempre usar drivers com suporte a queries parametrizadas e nunca interpolar input do usuário diretamente na query.

A pergunta que vale fazer antes de adotar NoSQL não é "NoSQL é melhor?", mas "qual é o padrão de acesso dominante da minha aplicação?". Se a resposta envolve joins complexos, relatórios ad hoc e consistência transacional forte, um banco relacional maduro provavelmente serve melhor — e adicionar complexidade operacional sem necessidade é um custo real. Se envolve esquemas que mudam frequentemente, volumes que exigem sharding, ou padrões de acesso que mapeariam naturalmente para chave-valor ou grafos, o modelo relacional é que vai criar atrito.

As melhores arquiteturas costumam usar os dois. Um PostgreSQL para o núcleo transacional, Redis para cache e sessões, e talvez um Elasticsearch para busca full-text — cada ferramenta no problema para o qual foi desenhada.

Referências

  • MongoDB. NoSQL Document Database. https://www.mongodb.com/
  • MongoDB. Document Validation and Indexing. https://docs.mongodb.com/manual/core/document-validation/
  • OWASP. SQL Injection Prevention Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
  • Martin Kleppmann. Designing Data-Intensive Applications. O'Reilly, 2017.