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.
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.
