10 min de leitura
postgres
bases de dados
arquitectura
saas
escalabilidade

Quando migrar de Postgres para uma base de dados distribuída

Sinais técnicos para sair de PostgreSQL, alternativas reais, custos escondidos e armadilhas de produção antes de escolher uma base distribuída.

Três pessoas de pé junto a uma parede de vidro com métricas de base de dados escritas a marcador

TL;DR

  • Migra de PostgreSQL só quando já esgotaste optimização, índices, particionamento, réplicas de leitura e separação de workloads.
  • A razão certa para uma base distribuída costuma ser uma destas: escrita multi-região, disponibilidade regional, volume de dados por shard, ou limites reais de throughput.
  • CockroachDB, YugabyteDB, Google Spanner, Citus e Vitess resolvem problemas diferentes. Não são “Postgres maior”.
  • O custo principal não é a licença. É latência, consistência, debugging, migrações, observabilidade e mudança no modelo mental da equipa.
  • Se o teu problema é uma query lenta ou falta de índices, uma base distribuída vai tornar o problema mais caro, não mais simples.

O erro comum: confundir crescimento com necessidade de distribuição

PostgreSQL aguenta mais do que a maioria das equipas imagina. Um PostgreSQL 16 bem configurado, com índices correctos, autovacuum afinado, queries revistas e hardware decente, consegue servir muitos produtos SaaS B2B, plataformas internas e aplicações de eCommerce sem precisar de uma base distribuída.

A pergunta certa não é “quando é que o Postgres deixa de escalar?”. A pergunta certa é: “qual é exactamente o limite que estamos a atingir?”.

Há limites muito diferentes:

  • CPU saturado por queries mal escritas.
  • I/O saturado por falta de índices ou excesso de writes.
  • Locks por transacções longas.
  • Tabelas grandes sem particionamento.
  • Latência elevada por clientes noutra região.
  • Necessidade de escrita activa em várias regiões.
  • Janela de manutenção demasiado curta para operações DDL.
  • Falta de isolamento entre workloads analíticos e transaccionais.

Só alguns destes justificam uma base distribuída.

Na prática, vejo demasiadas equipas saltarem para CockroachDB, YugabyteDB ou Spanner porque “vamos crescer”. Isso raramente é bom critério. Distribuição deve ser resposta a uma restrição concreta, medida e repetível. Se não consegues mostrar gráficos de CPU, IOPS, locks, p95, p99, tamanho de tabelas, volume de WAL e frequência de incidentes, ainda não tens diagnóstico. Tens ansiedade arquitectural.

Antes de migrar, esgota o Postgres normal

Antes de pensar numa base distribuída, há uma lista de trabalho sério em PostgreSQL que costuma resolver 80% dos casos.

Primeiro, índices. Não “meter índices em tudo”, mas índices alinhados com queries reais. pg_stat_statements é obrigatório. Sem ele, estás a optimizar por palpite. Em PostgreSQL 16, deves olhar para queries por tempo total, tempo médio, chamadas e leituras de disco.

SELECT
  query,
  calls,
  round(total_exec_time::numeric, 2) AS total_ms,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

Segundo, particionamento. Uma tabela com 500 GB não é automaticamente um problema, mas uma tabela com 500 GB, deletes frequentes, índices inchados e queries por intervalo temporal pode ser um problema evitável. Particionamento por mês ou por tenant pode reduzir scans, facilitar retenção e tornar manutenção mais previsível.

Terceiro, réplicas de leitura. Muitos sistemas misturam tráfego transaccional com dashboards, exports CSV, relatórios e integrações externas. Separar leituras pesadas para réplicas pode baixar a pressão no primário sem mudar o modelo de dados. Atenção à latência de replicação. Se a aplicação assume leitura imediata depois de escrita, podes introduzir bugs subtis.

Quarto, pooling. Em SaaS com Node.js, serverless ou muitos workers, já vi bases sofrerem mais por excesso de ligações do que por queries complexas. PgBouncer em transaction pooling pode ser a diferença entre 300 ligações caóticas e uma carga estável. Mas também tem trade-offs, por exemplo prepared statements e sessões com estado.

Quinto, hardware e configuração. Parece pouco sofisticado, mas passar de uma instância pequena para uma máquina com NVMe, RAM suficiente para working set e IOPS previsível é muitas vezes mais barato do que reescrever a camada de dados. Em cloud, a diferença entre storage generalista e storage provisionado pode ser brutal no p99.

A minha regra: se ainda não tens pg_stat_statements, backups testados, métricas de autovacuum, alertas de replication lag e explicações das 20 queries mais caras, não estás pronto para escolher uma base distribuída.

Sinais reais de que Postgres pode estar no limite

Há sinais que justificam abrir seriamente a discussão.

O primeiro é escrita que não cabe num único primário. PostgreSQL tradicional escala leitura relativamente bem com réplicas, mas escrita continua concentrada no primário. Se tens milhares de writes por segundo, contenção em índices, WAL muito elevado e não consegues reduzir carga por batching ou redesign, a distribuição pode fazer sentido. Ainda assim, convém separar “writes altos” de “writes mal modelados”. Inserir eventos append-only é diferente de actualizar as mesmas linhas concorrencialmente.

O segundo é multi-região activa. Se tens utilizadores na Europa e nos Estados Unidos com necessidade de escrever localmente e p99 abaixo de 150 ms, uma base central em eu-west-1 não chega. A latência física manda. Lisboa para Virgínia pode facilmente passar 80 ms só em rede. Com TLS, queries múltiplas e transacções, o p99 sobe depressa. Aqui, uma base como Google Spanner ou CockroachDB pode reduzir latência local, mas vai obrigar a pensar em consistência, conflitos e colocação de dados.

O terceiro é disponibilidade regional. Se a tua exigência é continuar a aceitar escritas quando uma região cloud cai, PostgreSQL com réplica síncrona noutra região pode funcionar, mas o custo em latência é alto e o failover é operacionalmente delicado. Bases distribuídas foram desenhadas para este tipo de cenário, embora não eliminem decisões difíceis.

O quarto é tamanho operacional. Não há um número mágico. 1 TB em PostgreSQL pode estar saudável. 200 GB podem estar um caos. Mas quando backups, restores, vacuum, reindex, migrações e alterações de schema já não cabem nas janelas operacionais, precisas de reavaliar arquitectura. Às vezes a resposta é particionamento ou arquivo. Outras vezes é sharding ou distribuição.

O quinto é isolamento por tenant. Em SaaS B2B, pode chegar o momento em que alguns clientes grandes perturbam todos os outros. Podes resolver com particionamento, bases separadas por tenant, schemas separados, ou sharding aplicacional. Uma base distribuída pode ajudar, mas nem sempre é a primeira opção.

Alternativas concretas e quando fazem sentido

CockroachDB é atractivo para equipas que querem SQL, transacções distribuídas e compatibilidade parcial com PostgreSQL. É bom candidato quando precisas de multi-região, disponibilidade e distribuição automática de dados. Mas não é PostgreSQL. Há diferenças em extensões, comportamento de queries, optimizações e latências de transacções. A documentação oficial em https://www.cockroachlabs.com/docs/ deve ser lida antes de assumir compatibilidade.

YugabyteDB segue uma linha parecida, com API compatível com PostgreSQL através de YSQL e arquitectura distribuída baseada em tablets. Pode ser interessante para workloads SQL distribuídos, especialmente quando a compatibilidade com Postgres é importante. Mas, como em CockroachDB, transacções distribuídas têm custo. Consulta https://docs.yugabyte.com/ antes de decidir.

Google Spanner é provavelmente a opção mais madura para consistência global com SQL distribuído, especialmente em Google Cloud. TrueTime é uma vantagem técnica real. Mas estás a comprar uma plataforma, não apenas uma base de dados. O modelo de custos, desenho de chaves primárias e dependência de cloud devem ser avaliados com frieza. Documentação: https://cloud.google.com/spanner/docs.

Citus, hoje parte do ecossistema Microsoft, é uma extensão que distribui PostgreSQL por shards. Faz sentido quando queres manter muito do modelo Postgres e tens uma chave de distribuição clara, por exemplo tenant_id. É excelente para alguns SaaS multi-tenant e workloads analíticos distribuídos. É menos elegante quando as queries cruzam tenants frequentemente ou quando não existe boa chave de distribuição. Documentação: https://docs.citusdata.com/.

Vitess é outra categoria. Nasceu para escalar MySQL, não PostgreSQL. É usado em ambientes de grande escala e suporta sharding, routing e operações online. Faz sentido se estás no mundo MySQL e tens equipa para operar essa complexidade. Não é resposta directa para quem tem PostgreSQL, mas é importante como referência arquitectural. Documentação: https://vitess.io/docs/.

A comparação prática:

OpçãoMelhor casoCusto escondido
PostgreSQL 16 com tuningSaaS B2B normal, eCommerce moderado, backoffice, fintech internaPrimário único para escrita
PostgreSQL com particionamento e réplicasLeituras pesadas, tabelas temporais, reporting separadoComplexidade aplicacional e lag de réplica
CitusMulti-tenant com boa chave de distribuiçãoQueries cross-shard e planeamento de dados
CockroachDB ou YugabyteDBSQL distribuído, disponibilidade, multi-regiãoLatência de transacções e diferenças face a Postgres
SpannerConsistência global em Google CloudCusto, desenho de schema e dependência da plataforma

O que muda tecnicamente numa base distribuída

A mudança mais importante é que a rede passa a fazer parte da base de dados. Num PostgreSQL clássico, uma transacção é local ao servidor. Numa base distribuída, uma transacção pode tocar vários nós, regiões e consensos internos. Isso tem impacto no p99.

Uma query que em PostgreSQL demora 15 ms pode passar para 80 ms se tocar múltiplos shards ou ranges. Uma transacção com cinco statements pode deixar de ser barata se envolver coordenação distribuída. O problema não é a média. É o p95 e o p99 em carga real.

Também muda o desenho de chaves. IDs sequenciais podem criar hotspots. Chaves aleatórias podem espalhar bem escrita, mas prejudicar localidade. Chaves compostas por tenant_id e tempo podem ser óptimas para SaaS, mas más para queries globais. Não há almoço grátis.

Outra mudança é a consistência. Muitas bases distribuídas prometem transacções ACID, mas tens de perceber o âmbito. Transacções dentro do mesmo shard podem ser baratas. Transacções cross-shard podem ser caras. Leituras “stale” podem ser aceitáveis para dashboards, mas não para pagamentos, saldos, inventário crítico ou permissões.

Em fintech e pagamentos, por exemplo, eu seria conservador. Não distribuiria escrita financeira sem uma razão fortíssima, testes de falha e reconciliação independente. Para saldos, ledgers e cobranças, a simplicidade operacional vale muito. Stripe, por exemplo, publica documentação extensa sobre idempotência e webhooks em https://docs.stripe.com/, e essa disciplina é tão importante como a base escolhida.

Também tens de repensar migrações. ALTER TABLE que era trivial pode tornar-se uma operação longa. Índices globais podem ter limitações. Backfills podem saturar clusters. E observabilidade deixa de ser “ver CPU da base”. Precisas de métricas por nó, shard, range, região, filas internas, retries, conflitos e latência de consenso.

Armadilhas de produção que custam caro

A armadilha mais comum é usar uma base distribuída como se fosse PostgreSQL com mais máquinas. A aplicação mantém joins grandes, transacções amplas, consultas sem chave de distribuição e jobs de backoffice que varrem tabelas inteiras. Resultado: mais nós, mais latência, mais custo e menos previsibilidade.

Outra armadilha é ignorar retries. Em sistemas distribuídos, erros transitórios são normais. Transacções podem falhar por contenção, mudança de leaseholder, conflito de serialização ou timeout. A aplicação tem de saber repetir operações idempotentes. Se o teu código não distingue erro permanente de erro transitório, vais ter incidentes estranhos.

Exemplo curto de padrão essencial em pseudocódigo TypeScript:

for (let attempt = 1; attempt <= 3; attempt++) {
  try {
    return await runTransaction()
  } catch (err) {
    if (!isRetryableDatabaseError(err) || attempt === 3) throw err
    await sleep(50 * attempt)
  }
}

O detalhe importante não é o snippet. É a disciplina: operações repetíveis, chaves de idempotência, timeouts explícitos e logs com correlation id.

Outra gotcha: timestamps. Em sistemas multi-região, confiar cegamente em now() para ordenar eventos de negócio pode ser perigoso. Algumas bases dão garantias fortes, outras não. Mesmo com garantias, a semântica de “aconteceu antes” deve ser modelada. Para eventos críticos, usa versões, sequências por entidade ou ledgers append-only.

Também há a armadilha do custo. Um PostgreSQL gerido pode custar algumas centenas de euros por mês num produto em crescimento. Um cluster distribuído multi-região, com tráfego entre regiões, observabilidade e overprovisioning para falhas, pode multiplicar isso várias vezes. O custo é aceitável quando compra disponibilidade ou latência que o negócio realmente precisa. É desperdício quando compra tranquilidade psicológica.

Um processo de decisão que funciona

Eu usaria uma sequência simples.

Primeiro, mede. Recolhe 14 a 30 dias de métricas: CPU, memória, IOPS, tamanho de tabelas e índices, WAL gerado por hora, queries mais caras, locks, deadlocks, replication lag, p95 e p99 por endpoint crítico. Sem isto, a decisão é fraca.

Segundo, classifica o problema. É leitura, escrita, latência geográfica, disponibilidade, tamanho de dados, isolamento por cliente, ou operação? Cada categoria aponta para soluções diferentes.

Terceiro, tenta a solução menos distribuída. Índices, query rewrite, particionamento, réplicas, filas, cache, arquivo frio, materialized views, read models, ou separação OLTP/OLAP. Muitas vezes, mover reporting para ClickHouse, BigQuery ou DuckDB sobre object storage resolve mais do que trocar a base transaccional.

Quarto, faz um teste com workload real. Não uses apenas benchmarks sintéticos. Reproduz as 20 queries mais importantes, os jobs de background, picos de escrita e migrações. Mede p50, p95, p99 e taxa de retries. Um bom alvo inicial pode ser p99 abaixo de 200 ms para operações críticas dentro da mesma região, mas depende do produto.

Quinto, desenha falhas. Desliga um nó. Simula perda de região. Aumenta latência entre regiões. Faz deploy durante backfill. Restaura backup. Se a equipa não consegue operar estes cenários em ambiente de teste, não está pronta para produção.

Sexto, calcula custo total. Inclui licenças, cloud, tráfego inter-região, observabilidade, tempo de engenharia, formação, migração, dual writes temporários, rollback e suporte. A pergunta não é “qual é a tecnologia melhor?”. É “qual é o menor sistema que cumpre os requisitos por 18 a 24 meses?”.

A minha opinião é simples: PostgreSQL deve ser a escolha por defeito até prova em contrário. A base distribuída entra quando há uma restrição que o Postgres não consegue resolver sem criar mais risco do que remove. Não antes.

Conclusão

Migrar de PostgreSQL para uma base distribuída é uma decisão de arquitectura, não um rito de passagem. Se o problema é local, resolve localmente. Se o problema é multi-região, disponibilidade forte ou escrita acima do que um primário suporta, então vale a pena avaliar alternativas.

A pior migração é a que troca uma base conhecida por um sistema distribuído mal compreendido. Se estás a enfrentar um problema parecido, marca uma conversa em https://impact-origin.com/agendamento.

Impact OriginGostaste deste artigo?Na Impact Origin ajudamos fundadores e equipas a construir e escalar software à medida, do MVP da startup ao próximo passo. Se tens um projeto em mente, vamos falar.