Database

Escopo: transversal. Aplica-se a qualquer linguagem ou stack do projeto.

O banco de dados é o componente com maior impacto em performance e o mais difícil de escalar retroativamente. Escolher o modelo certo, escrever queries eficientes e saber diagnosticar gargalos antes de chegarem à produção são habilidades que mudam a capacidade de um sistema.

Conceitos fundamentais

ConceitoO que é
SQL (Structured Query Language, Linguagem de Consulta Estruturada)Linguagem padrão para bancos relacionais; define, consulta e manipula dados em tabelas
NoSQL (Not Only SQL, Não Apenas SQL)Família de bancos não-relacionais: document, key-value, column-family e graph
ACID (Atomicity, Consistency, Isolation, Durability, Atomicidade, Consistência, Isolamento, Durabilidade)Garantias de transação que asseguram integridade dos dados em bancos relacionais
Index (Índice)Estrutura auxiliar que acelera buscas em uma coluna sem varrer a tabela inteira
Full scan (varredura completa)Leitura de todas as linhas da tabela para encontrar os registros; evitar em tabelas grandes
EXPLAIN (explicar plano)Comando que mostra o plano de execução de uma query sem executá-la
Query plan (plano de execução)Sequência de operações que o banco escolhe para executar uma query
Seq Scan (varredura sequencial)Leitura linha a linha da tabela; indica ausência de índice útil
Index Scan (varredura por índice)Leitura via índice; muito mais eficiente que Seq Scan para filtros seletivos
N+1 (consulta repetida em loop, anti-padrão)Anti-padrão que executa uma query por item de uma lista em vez de uma única query em lote
Slow query log (log de queries lentas)Registro automático de queries que excedem um tempo limite configurado
Lock (bloqueio)Mecanismo que impede acesso simultâneo conflitante a um recurso; pode causar espera ou deadlock
Deadlock (bloqueio morto)Situação onde duas transações esperam uma pela outra indefinidamente
Connection pool exhaustion (esgotamento do pool de conexões)Todas as conexões do pool estão em uso; novas requisições ficam em fila ou falham
Projection (projeção)Define quais campos retornar em uma consulta NoSQL; evita trafegar o documento inteiro
Aggregation pipeline (pipeline de agregação)Sequência de estágios para processar documentos em lote no MongoDB; substitui JOINs e GROUP BY do SQL
ETL (Extract, Transform, Load, Extração, Transformação e Carga)Processo de mover dados de fontes externas para o banco: extrair da origem, transformar e carregar no destino. Ver etl-bi.md
Staging table (tabela de preparação)Tabela intermediária que recebe dados brutos antes de validar e inserir na tabela de produção
Chunk (fatia, lote)Subconjunto fixo de linhas processado por vez em operações de alto volume; mantém locks de curta duração

SQL vs NoSQL

A escolha não é sobre modernidade: é sobre o modelo de dados e os padrões de acesso.

Bancos Relacionais (SQL)

Dados estruturados em tabelas com schema definido. Relações entre entidades expressas como foreign keys. Transações com garantias ACID.

Ponto forteDetalhe
Consistência forteTransações garantem estado correto mesmo em falhas
Queries ad-hocSQL permite explorar dados sem planejamento prévio de acesso
JoinsRelacionamentos complexos consultados sem duplicar dados
Ferramentas madurasOtimizadores, planos de execução, backups, replicação

Exemplos: PostgreSQL, SQL Server, MySQL, SQLite.

Quando usar: domínio com relações entre entidades, necessidade de consistência transacional, queries variadas não definidas em tempo de design.

Bancos Não-Relacionais (NoSQL)

Quatro modelos principais, cada um otimizado para um padrão de acesso diferente:

ModeloComo organiza os dadosMelhor para
Document (documento)Documentos JSON aninhados, sem schema rígidoDados hierárquicos com estrutura variável (catálogo, CMS, perfis)
Key-Value (chave-valor)Acesso por chave única, valor opacoCache, sessão, contadores, filas simples
Column-Family (família de colunas)Colunas agrupadas, leitura eficiente em colunas específicasAnalytics, séries temporais, telemetria em alta escala
Graph (grafo)Nós e arestas como cidadãos de primeira classeRedes sociais, recomendação, detecção de fraude

Quando usar: escala de escrita muito alta que bancos relacionais não absorvem, dados sem schema previsível, padrão de acesso fixo e conhecido (key-value, full document), ou quando o modelo de grafo representa a estrutura natural dos dados.

O que não fazer: escolher NoSQL porque "escala melhor" sem medir. Banco relacional bem indexado suporta volumes muito maiores do que a maioria dos projetos vai atingir. A complexidade operacional de NoSQL tem custo real.


Tuning de Queries

O gargalo mais comum não é hardware: é query mal escrita ou ausência de índice.

Índices

Um índice cria uma estrutura auxiliar que o banco usa para encontrar linhas sem varrer a tabela inteira.

TipoQuando usar
Índice simplesFiltros e ordenações em uma única coluna frequente no WHERE ou ORDER BY
Índice compostoQueries que filtram por duas ou mais colunas juntas; a ordem das colunas importa
Índice coveringIndex que inclui todas as colunas da query; leitura sem tocar a tabela principal
Índice parcialIndex sobre um subconjunto de linhas (WHERE status = 'active'); menor e mais rápido
-- índice simples: acelera buscas por email
CREATE INDEX idx_users_email ON users(email);

-- índice composto: acelera WHERE status = ? AND created_at > ?
CREATE INDEX idx_orders_status_created ON orders(status, created_at);

Regras:

  • Indexar colunas usadas em WHERE, JOIN e ORDER BY com alta seletividade
  • Não indexar colunas com poucos valores distintos (boolean, gender); o banco prefere full scan
  • Índices têm custo de escrita: cada INSERT/UPDATE/DELETE atualiza todos os índices da tabela
  • Colunas com função no WHERE desativam o índice: WHERE LOWER(email) = ? não usa índice em email; criar índice funcional ou normalizar no insert. O mesmo vale para CAST/CONVERT na coluna: converter o parâmetro, nunca a coluna. Ver sql/performance.md

Boas práticas de query

Padrões com BAD/GOOD completos: sql/conventions/advanced/performance.md.

Consultas NoSQL

Os anti-padrões de NoSQL diferem dos SQL, mas o princípio é o mesmo: trabalho desnecessário no servidor é mais barato que trabalho no cliente.

Guia completo por SGBD: docs/nosql/. Convenções de CRUD (Create Read Update Delete, Criar Ler Atualizar Excluir), naming e performance: nosql/conventions/.

❌ Ruim: sem projeção: trafega o documento inteiro para usar um campo
const user = await database.collection('users').findOne({ email });
const userName = user.name;
✅ Bom: projeção limita os campos retornados
class UserRepository {
  async findByEmail(email) {
    const user = await this.collection.findOne(
      { email },
      { projection: { name: 1, _id: 0 } }
    );

    return user;
  }
}
❌ Ruim: filtro em memória: carrega a coleção inteira para filtrar no cliente
const allOrders = await database.collection('orders').find({}).toArray();
const pendingOrders = allOrders.filter(order => order.status === 'pending');
✅ Bom: filtro na query: banco usa índice em status
class OrderRepository {
  async findPending() {
    const pendingOrders = await this.collection
      .find({ status: 'pending' })
      .project({ id: 1, customerId: 1, total: 1 })
      .toArray();

    return pendingOrders;
  }
}
❌ Ruim: N+1 em document store: uma query por item para buscar documento relacionado
const orders = await database.collection('orders').find({ userId }).toArray();

const enrichedOrders = await Promise.all(
  orders.map(async order => {
    const product = await database.collection('products').findOne({ _id: order.productId });

    return { ...order, product };
  })
);
✅ Bom: $lookup resolve em uma única passagem no banco
class OrderRepository {
  async findByUserWithProduct(userId) {
    const enrichedOrders = await this.collection.aggregate([
      { $match: { userId } },
      {
        $lookup: {
          from: 'products',
          localField: 'productId',
          foreignField: '_id',
          as: 'product',
        },
      },
      { $unwind: '$product' }, // drops orders with no matching product: use preserveNullAndEmptyArrays: true if product can be missing
    ]).toArray();

    return enrichedOrders;
  }
}

Operações em Lote

Operações em lote agrupam múltiplas linhas em uma única instrução ou dividem uma operação grande em ciclos menores. Dois objetivos distintos: aumentar throughput em carga inicial e evitar locks de longa duração em operações de manutenção.

PadrãoQuando usar
Batch INSERTInserir muitos registros de uma vez: importação, ETL, seed de dados
Chunked UPDATE/DELETEAtualizar ou remover grandes volumes sem bloquear a tabela por minutos
BULK INSERT / COPYImportar arquivos CSV ou binários diretamente no banco, sem round trips pela aplicação
Staging tableValidar dados externos antes de inserir na tabela de produção
Scheduled jobExecutar operações periódicas; limpeza, agregação, archive; sem intervenção manual

Chunk size

Não existe valor universal. O critério é o tempo de lock aceitável para o sistema.

  • Lotes entre 1.000 e 5.000 linhas são um ponto de partida seguro para a maioria dos casos
  • Lotes muito pequenos aumentam o número de round trips e o overhead de transação
  • Lotes muito grandes seguram o lock por mais tempo e aumentam o risco de rollback custoso

Operações em lote que rodam em produção precisam de idempotência: se o job for interrompido, a próxima execução deve continuar de onde parou sem duplicar ou corromper dados. O padrão é usar a condição de filtro do próprio UPDATE/DELETE como cursor natural: o WHERE já exclui as linhas já processadas nas iterações anteriores.

Padrões de query: sql/conventions/advanced/batch.md.

Recursos específicos por banco: SQL Server | PostgreSQL.


Plano de Execução

O plano de execução mostra como o banco vai executar a query: quais índices vai usar, como vai fazer joins, qual o custo estimado de cada operação.

Antes de deployar qualquer query em uma tabela grande, analisar o plano.

Sintaxe por banco: PostgreSQL | SQL Server.

O que procurar no plano

OperaçãoO que significaAção
Seq ScanVarredura completa, sem índice útilCriar índice na coluna de filtro
Index ScanUsa índice, lê linhas da tabelaBom; considerar Index Only Scan se possível
Index Only ScanUsa índice sem tocar a tabelaÓtimo (query coberta pelo índice)
Nested LoopJoin com loop para cada linha externaAceitável para tabelas pequenas; ruim para grandes
Hash JoinConstrói hash table em memória para o joinEficiente para joins de tabelas grandes
High costNúmero alto na estimativa de custoPonto de partida para investigar
-- exemplo de saída EXPLAIN (PostgreSQL)
Seq Scan on orders  (cost=0.00..4521.00 rows=150000 width=16)
  Filter: ((status)::text = 'pending'::text)

Seq Scan em tabela grande com Filter é o sinal mais claro de índice ausente.


Troubleshooting de Gargalos

Slow query log

O primeiro passo para identificar problemas em produção é habilitar o log de queries lentas.

Configuração por banco: PostgreSQL | SQL Server.

N+1 em runtime

N+1 raramente aparece no código; aparece no log de queries. O sinal é um padrão repetido de queries idênticas com IDs diferentes em sequência rápida.

-- sinal de N+1 no log
SELECT * FROM customers WHERE id = 1  -- 0.1ms
SELECT * FROM customers WHERE id = 2  -- 0.1ms
SELECT * FROM customers WHERE id = 3  -- 0.1ms
... (100 vezes)

Ferramentas que expõem N+1 automaticamente: Bullet (Rails), Hibernate Statistics, EF Core logging, Sequelize logging mode.

Connection pool exhaustion

Quando todas as conexões do pool estão em uso, novas requisições ficam em fila. Sintoma: timeouts em requisições simples que normalmente são rápidas, sem aumento de CPU (Central Processing Unit, Unidade Central de Processamento) ou lentidão de queries.

Diagnóstico por banco: PostgreSQL | SQL Server.

Causas comuns: queries longas segurando conexão, transaction não fechada, pool subdimensionado, pico de tráfego inesperado.

Locks e deadlocks

Lock é esperado: é o mecanismo de consistência. O problema é lock de longa duração ou deadlock.

Identificar locks ativos: PostgreSQL | SQL Server.

Deadlock aparece no log com mensagem explícita. A causa mais comum é duas transações acessando as mesmas linhas em ordem inversa. A solução é padronizar a ordem de acesso.

-- padrão que gera deadlock
Transação A: lock em orders(1) → tenta lock em payments(1)
Transação B: lock em payments(1) → tenta lock em orders(1)  ← deadlock

-- solução: sempre adquirir locks na mesma ordem
Transação A: lock em orders(1) → lock em payments(1)
Transação B: lock em orders(1) → lock em payments(1)  ← espera A terminar

Checklist de investigação

Ao receber relatório de "banco lento":

  1. Verificar slow query log (query específica ou carga geral?)
  2. EXPLAIN ANALYZE na query suspeita (Seq Scan em tabela grande?)
  3. Verificar conexões ativas (pool exhaustion?)
  4. Verificar locks de longa duração (transação presa?)
  5. Verificar volume de dados (a tabela cresceu e o índice ficou ineficiente?)
  6. Verificar índices existentes na tabela (algum foi dropado ou nunca foi criado?)

Referência rápida

ProblemaSinalAção
Query lentaSeq Scan no EXPLAIN, timeout em produçãoCriar índice na coluna de filtro
N+1Queries repetidas com IDs sequenciais no logEager load em lote; JOIN ou IN clause
Pool exhaustionTimeout sem lentidão de queryAumentar pool, investigar queries longas
DeadlockErro explícito no logPadronizar ordem de acesso às linhas
Índice não usadoIndex Scan esperado mas Seq Scan no planoVerificar função em coluna, tipo de dado, seletividade
Crescimento de tabelaQuery ficou lenta sem mudança de códigoAnalisar plano novamente (estatísticas podem estar desatualizadas); rodar ANALYZE

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.