Pular para o conteúdo
banco-de-dados

Como Escalar Banco de Dados: Estratégias para Aplicações de Alto Volume

Estratégias práticas para escalar banco de dados: read replicas, sharding, cache, particionamento e quando cada abordagem faz sentido.

Douglas M. Pereira4 min de leitura
banco de dadosescalabilidademysqlpostgresqlredisperformance

O banco de dados é o gargalo mais comum em aplicações que crescem

Aplicações costumam ter gargalo no banco antes de ter gargalo no servidor de aplicação. A razão é simples: adicionar servidores de aplicação é fácil (horizontal scaling); adicionar nós de banco requer planejamento, replicação e gestão de consistência.

Mas existe um caminho incremental que resolve a maioria dos casos sem precisar de arquitetura distribuída complexa.

A sequência certa de escala

Nível 0: Otimize antes de escalar

Antes de qualquer infraestrutura nova, certifique-se de que:

  • Queries lentas estão identificadas e otimizadas
  • Índices adequados estão criados
  • Configurações do InnoDB/PostgreSQL estão ajustadas para o hardware disponível
  • N+1 queries estão eliminadas

60–70% dos problemas de performance de banco são de query, não de infraestrutura.

Nível 1: Read Replicas

Para workloads leitura-intensiva (a maioria das aplicações web business):

 Aplicação
    │
    ├── Writes ──────► Master
    │                     │ replicação
    └── Reads ──────┬─► Replica 1
                    └─► Replica 2
// Configuração de múltiplas conexões
const writePool = mysql.createPool(process.env.DATABASE_PRIMARY_URL)
const readPool = mysql.createPool(process.env.DATABASE_REPLICA_URL)

// Uso: writes no master, reads na replica
async function getOrders(userId: string) {
  return readPool.query('SELECT * FROM orders WHERE user_id = ?', [userId])
}

async function createOrder(data: OrderData) {
  return writePool.query('INSERT INTO orders ...', [data])
}

Atenção: replicação é assíncrona. Após um write, não leia imediatamente da replica — pode não estar replicado ainda. Para reads pós-write, use o master por alguns segundos.

Nível 2: Cache de resultados com Redis

Redis resolve a maioria dos problemas de leitura sem precisar escalar o banco:

const CACHE_TTL = 300  // 5 minutos

async function getProductById(id: string) {
  const cacheKey = `product:${id}`
  
  // Tentar o cache primeiro
  const cached = await redis.get(cacheKey)
  if (cached) return JSON.parse(cached)
  
  // Cache miss: buscar no banco
  const product = await db.products.findUnique({ where: { id } })
  if (!product) return null
  
  // Armazenar no cache
  await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(product))
  return product
}

// Invalidar cache quando o produto muda
async function updateProduct(id: string, data: Partial<Product>) {
  await db.products.update({ where: { id }, data })
  await redis.del(`product:${id}`)  // invalida cache
}

Cache é poderoso mas tem armadilhas:

  • Invalidação incorreta gera dados desatualizados
  • Cache grande consome memória
  • Cache miss storm (muitos pedidos simultâneos ao mesmo item expirado) pode sobrecarregar o banco

Nível 3: Particionamento de tabelas

Para tabelas muito grandes (> 100M de registros), o particionamento físico pode ajudar:

-- Particionamento por range de data
CREATE TABLE orders (
  id BIGINT NOT NULL,
  user_id INT NOT NULL,
  created_at DATETIME NOT NULL,
  ...
) PARTITION BY RANGE (YEAR(created_at)) (
  PARTITION p2023 VALUES LESS THAN (2024),
  PARTITION p2024 VALUES LESS THAN (2025),
  PARTITION p2025 VALUES LESS THAN (2026),
  PARTITION p_future VALUES LESS THAN MAXVALUE
);

Queries com filtro na coluna de particionamento só varrem a partição pertinente.

Nível 4: Separação de workloads por banco

Workloads com perfis muito diferentes se beneficiam de bancos diferentes:

OLTP (transacional, tempo real) → MySQL/PostgreSQL
Análises/BI (queries pesadas, históricas) → ClickHouse, DuckDB, BigQuery
Busca full-text → Elasticsearch, Typesense, Meilisearch
Dados de gráficos/relacionamentos → Neo4j
Time-series (métricas, logs) → InfluxDB, TimescaleDB

Mover as queries analíticas para um banco colunar pode libertar o banco transacional de queries que demoram minutos.

Nível 5: Sharding (raramente necessário)

Para volumes extremos onde nenhuma outra estratégia funciona, sharding distribui dados entre múltiplos bancos:

Shard 1: user_id % 4 == 0  → DB Server 1
Shard 2: user_id % 4 == 1  → DB Server 2
...

A complexidade é enorme: joins cross-shard são impossíveis, migrações são caras, queries que não usam a chave de sharding viram broadcasts. Evite até ser absolutamente necessário.

Monitoramento de crescimento

Cheque mensalmente:

-- Tamanho de tabelas (MySQL)
SELECT 
  table_name,
  table_rows,
  ROUND(data_length / 1024 / 1024, 2) data_mb,
  ROUND(index_length / 1024 / 1024, 2) index_mb
FROM information_schema.tables
WHERE table_schema = 'seu_banco'
ORDER BY data_length DESC;

-- Queries mais lentas (avg_time > 1s)
SELECT query, count_star, avg_timer_wait/1e12 avg_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE avg_timer_wait > 1e12
ORDER BY avg_timer_wait DESC
LIMIT 20;

Conclusão

A maioria das empresas resolve problemas de banco com otimização de queries, índices e cache Redis — sem precisar de arquitetura distribuída. Siga a sequência dos níveis: otimize primeiro, adicione replicas quando necessário, use cache agressivamente e só parta para sharding quando todas as outras opções estiverem esgotadas.