Artigo

Seu Sistema Está Lento? O Problema Não É o Banco de Dados — É Quando Você Processa os Dados

11 min de leitura

Todo sistema começa pequeno. Mil registros, dez mil, cem mil.

Tudo responde rápido. O usuário clica e a informação aparece. Você nem pensa em performance porque não precisa.

Aí o sistema cresce.

O banco acumula milhões de registros e um belo dia, alguém abre o dashboard, aplica um filtro de janeiro a dezembro e espera.

E espera. E toma timeout. E dá F5. E abandona.

A reação natural é culpar o banco de dados, trocar o plano, adicionar índice, jogar mais RAM... Mas o problema real é outro: você está fazendo o processamento pesado no momento em que o usuário pede a informação. E isso não escala.

Nesse artigo eu vou te mostrar o padrão que sistemas grandes usam pra lidar com milhões (ou bilhões) de registros sem que o usuário perceba. E o melhor: não precisa ser um engenheiro do Google pra aplicar isso.


O Problema: Processar na Hora Errada

Pra entender o problema, imagina um cenário simples.

Você tem uma tabela de transações financeiras. Cada depósito, cada saque, cada transferência gera um registro. Pra saber o saldo, o sistema precisa somar todas as transações desde o início da conta.

Com mil transações, isso é instantâneo. Com cem milhões? Inviável em tempo real.

Visualmente, o fluxo problemático é esse:

O problema não é o banco. É a estratégia. Você está pedindo pro banco fazer um trabalho gigantesco toda vez que alguém clica num botão.


A Solução: Processar Antes, Servir Rápido

A ideia é simples: separe o processamento pesado da resposta ao usuário.

Um processo roda em background — pode ser uma vez por hora, uma vez por dia, depende do caso — e consolida os dados numa tabela de rollup. Quando o usuário pede o saldo, o sistema consulta o rollup e calcula apenas as transações que aconteceram depois do último processamento.

Ao invés de varrer anos de dados, ele varre horas. Ou minutos.

Percebe a diferença? O trabalho pesado já foi feito. A resposta pro usuário é quase instantânea.


Como Funciona uma Tabela de Rollup

A tabela de rollup é basicamente um checkpoint. Ela guarda o resultado de um cálculo pesado num ponto específico no tempo.

Estrutura da tabela

1CREATE TABLE saldo_rollups ( 2 id BIGINT PRIMARY KEY IDENTITY, 3 conta_id BIGINT NOT NULL, 4 saldo_acumulado DECIMAL(18,2) NOT NULL, 5 calculado_em DATETIME2 NOT NULL, 6 transacao_ate_id BIGINT NOT NULL, -- última transação incluída no cálculo 7 8 INDEX IX_rollup_conta (conta_id, calculado_em DESC) 9);

O job que gera o rollup

1public class RollupJob 2{ 3 private readonly AppDbContext _db; 4 5 public RollupJob(AppDbContext db) => _db = db; 6 7 public async Task ExecutarAsync() 8 { 9 // Pega todas as contas que têm transações novas 10 var contasComMovimentacao = await _db.Transacoes 11 .Where(t => t.Data > _db.SaldoRollups 12 .Where(r => r.ContaId == t.ContaId) 13 .Max(r => (DateTime?)r.CalculadoEm) ?? DateTime.MinValue) 14 .Select(t => t.ContaId) 15 .Distinct() 16 .ToListAsync(); 17 18 foreach (var contaId in contasComMovimentacao) 19 { 20 // Busca o último rollup dessa conta 21 var ultimoRollup = await _db.SaldoRollups 22 .Where(r => r.ContaId == contaId) 23 .OrderByDescending(r => r.CalculadoEm) 24 .FirstOrDefaultAsync(); 25 26 var saldoAnterior = ultimoRollup?.SaldoAcumulado ?? 0; 27 var dataCorte = ultimoRollup?.CalculadoEm ?? DateTime.MinValue; 28 29 // Soma só as transações novas 30 var delta = await _db.Transacoes 31 .Where(t => t.ContaId == contaId && t.Data > dataCorte) 32 .SumAsync(t => t.Valor); 33 34 // Grava o novo rollup 35 _db.SaldoRollups.Add(new SaldoRollup 36 { 37 ContaId = contaId, 38 SaldoAcumulado = saldoAnterior + delta, 39 CalculadoEm = DateTime.UtcNow 40 }); 41 } 42 43 await _db.SaveChangesAsync(); 44 } 45}

A consulta rápida

1public async Task<decimal> ObterSaldoAsync(long contaId) 2{ 3 // Pega o último rollup 4 var rollup = await _db.SaldoRollups 5 .Where(r => r.ContaId == contaId) 6 .OrderByDescending(r => r.CalculadoEm) 7 .FirstOrDefaultAsync(); 8 9 var saldoBase = rollup?.SaldoAcumulado ?? 0; 10 var dataCorte = rollup?.CalculadoEm ?? DateTime.MinValue; 11 12 // Soma só o que veio depois 13 var delta = await _db.Transacoes 14 .Where(t => t.ContaId == contaId && t.Data > dataCorte) 15 .SumAsync(t => t.Valor); 16 17 return saldoBase + delta; 18}

O ponto chave: a query do rollup varre milhões de registros, mas roda em background, sem pressa. A query do usuário varre dezenas ou centenas de registros — só o delta desde o último processamento.


Background Jobs: O Trabalho Pesado Acontece nos Bastidores

O rollup é um tipo de background job. Mas o padrão vai muito além de saldos financeiros.

Cenário: Monitoramento Industrial com Milhares de Sensores

Imagina uma operação industrial com 5 mil sensores: temperatura, pressão, vibração, umidade. Cada sensor manda dados o tempo todo. Se você tentar gravar tudo direto num banco relacional e calcular em tempo real, vai estourar.

O que sistemas assim fazem:

Cada camada resolve um problema diferente:

CamadaO que fazPor que existe
Message BrokerAbsorve o volume de entrada dos sensoresDesacopla produtores de consumidores. Se um worker cai, as mensagens ficam na fila
WorkersProcessam dados em paralelo, cada um especializadoDistribui a carga. Se temperatura demora mais, não trava pressão
Rollup DBConsolida dados históricos em intervalosTransforma bilhões de leituras em milhares de agregações consultáveis
CacheGuarda os resultados mais acessadosEvita consultas repetidas ao banco. Dashboard abre em milissegundos

Outros cenários comuns

O padrão se repete em vários contextos:


Anatomia de um Sistema com Background Jobs

Quando você junta essas peças, a arquitetura completa fica assim:

Dois mundos separados:

  • Camada do usuário: rápida, leve, só consulta dados pré-processados
  • Camada de background: pesada, sem pressa, faz o trabalho bruto quando ninguém está esperando

Quando Vale a Pena (e Quando Não Vale)

Essa arquitetura não é pra todo mundo. Se seu sistema tem poucos dados e responde bem, adicionar workers e rollups é over-engineering puro.

A regra é simples: se o processamento é pesado e o resultado não muda a cada segundo, não faz sentido recalcular tudo toda vez que alguém pede. Processa antes, guarda o resultado, serve rápido.

Sinais de que você precisa dessa arquitetura

SinalO que aconteceO que fazer
Queries lentas que pioram com o tempoDashboard que levava 200ms agora leva 8sRollup + cache
Timeout em relatóriosUsuário clica em "gerar relatório" e toma 504Background job + notificação quando pronto
CPU do banco no teto em horário comercialConsultas analíticas competindo com operações transacionaisSeparar leitura pesada em jobs off-peak
Usuários reclamando de lentidãoO sistema "trava" em horários de picoCache + pré-processamento

Na Prática: Ferramentas por Stack

As ferramentas variam conforme a stack, mas o conceito é sempre o mesmo.

.NET

1// Com Hangfire — agendar um job recorrente 2RecurringJob.AddOrUpdate<RollupJob>( 3 "rollup-saldos", 4 job => job.ExecutarAsync(), 5 Cron.Hourly); // roda a cada hora

Node.js

1// Com BullMQ — adicionar job numa fila 2import { Queue } from 'bullmq'; 3 4const rollupQueue = new Queue('rollup'); 5 6// Agendar job recorrente 7await rollupQueue.add('consolidar-saldos', 8 { tipo: 'saldos' }, 9 { repeat: { every: 3600000 } } // a cada 1 hora 10);

Python

1# Com Celery — task periódica 2from celery import Celery 3from celery.schedules import crontab 4 5app = Celery('tasks') 6 7@app.on_after_configure.connect 8def setup_periodic_tasks(sender, **kwargs): 9 sender.add_periodic_task( 10 crontab(minute=0), # a cada hora cheia 11 gerar_rollup.s() 12 ) 13 14@app.task 15def gerar_rollup(): 16 # lógica de consolidação aqui 17 pass

Outras opções

FerramentaStack / ContextoTipo
Hangfire.NETLibrary (roda dentro da app)
Quartz.NET.NETLibrary (mais configurável)
BullMQNode.jsFila + workers com Redis
CeleryPythonTask queue distribuída
SidekiqRubyBackground jobs com Redis
Azure FunctionsCloud (Azure)Serverless com timer trigger
AWS Lambda + EventBridgeCloud (AWS)Serverless com schedule
Cron jobLinux / qualquerO clássico. Simples e funciona

O conceito é o mesmo independente da tecnologia: separar o processamento pesado da resposta ao usuário. Um acontece em background, no tempo dele. O outro acontece na hora, e precisa ser rápido.


Quem Faz Isso no Mundo Real

Stack Overflow, Reddit, Instagram, Amazon, Mercado Livre. Todos processam bilhões de dados por dia. Nenhum deles faz o cálculo na hora que o usuário clica.

  • Netflix: pré-calcula recomendações em batch jobs massivos. Quando você abre o app, o catálogo personalizado já está pronto.
  • Mercado Livre: consolida métricas de vendedores (reputação, tempo de envio, taxa de reclamação) em background. Se calculasse em tempo real pra cada busca, a página nunca carregaria.
  • Nubank: saldo, extrato, limites — tudo consolidado em pipelines de dados que rodam continuamente. O app só consulta o resultado.

Se eles fizessem o processamento na hora do clique, você já teria reclamado.


Resumo Visual


Conclusão

Parece complexo visto de fora. Mas cada peça encaixa na outra.

O broker absorve o volume. Os workers distribuem o processamento. O rollup consolida. O cache serve rápido. E o usuário nem percebe que por trás de um clique tem uma arquitetura inteira trabalhando nos bastidores.

Você não precisa implementar tudo de uma vez. Começa pelo rollup. Coloca um job simples que roda de hora em hora. Mede a diferença. Quando sentir necessidade, adiciona cache. Depois workers. Cada camada resolve um problema novo.

O segredo não é usar a ferramenta mais sofisticada. É entender quando processar. E a resposta quase sempre é: não na hora que o usuário pediu.

Racoelho

Conteúdo sobre desenvolvimento, tecnologia e desafios de programação para impulsionar sua carreira em tech.

Conecte-se

© 2024- 2026 Racoelho. Todos os direitos reservados.

v3.0.15 • Build: 2026-03-17