Serviços · Otimização de banco de dados

Banco lento não se resolve trocando de instância

Engenharia de performance de banco — diagnóstico com dados, não chute. A gente mede onde o tempo vai antes de tocar em qualquer linha de SQL ou subir o tamanho da instância.

O problema que vemos no mercado

O sintoma quase sempre chega igual: uma query que demora 8 segundos trava o checkout no horário de pico, o time abre um chamado, e a única resposta que veio do fornecedor de cloud (ou do próprio time, sem ferramenta de diagnóstico) foi "compra uma instância maior". A fatura sobe, o problema some por algumas semanas enquanto sobra folga de CPU, e volta exatamente igual quando o volume cresce de novo. Hardware comprou tempo, não consertou nada.

Quando a gente abre o banco, encontra quase sempre os mesmos padrões. N+1 do ORM: a tela carrega uma lista e dispara mais uma query por linha, então a página que devia fazer 2 consultas faz 400. Índices ausentes onde a query mais cara faz sequential scan numa tabela de milhões de linhas — e, ao mesmo tempo, índices redundantes que ninguém usa, ocupando disco e penalizando todo INSERT. `pg_stat_statements` desligado, então ninguém sabe quais são as 10 queries que comem 80% do tempo total do banco.

E tem o que é silencioso. Lock contention: duas transações disputam a mesma linha e uma fica esperando, sem erro nenhum no log da aplicação — só latência que aparece e some sem explicação. Banco crescendo sem particionamento, com uma tabela de eventos de 200 milhões de linhas onde cada query de relatório varre o histórico inteiro. E o pool de conexões esgotado porque a aplicação abre conexão direto no Postgres sem pooler — cada worker segura uma conexão, o limite estoura, e o banco passa a recusar conexão enquanto a CPU está em 20%.

O resultado é previsível: o time trata sintoma com upgrade de instância, o custo de cloud sobe sem teto, e a causa raiz — modelagem, índice, plano de execução — continua intocada porque ninguém olhou o EXPLAIN.

Como entregamos

Começamos por baseline com dados reais, nunca por palpite. Ligamos `pg_stat_statements` para ver quais queries consomem mais tempo total e quais têm a pior média, e `auto_explain` para capturar o plano das que estourarem um limite de tempo em produção. Antes de tocar em qualquer índice, sabemos exatamente onde o banco gasta o tempo dele — porque otimizar a query errada é trabalho jogado fora.

Em cima desse ranking, rodamos `EXPLAIN (ANALYZE, BUFFERS)` nas top-N queries. O `ANALYZE` mostra o tempo real de cada nó do plano, o `BUFFERS` mostra quanto bloco veio do cache e quanto veio do disco — é a diferença entre uma query que parece cara mas está quente em memória e uma que está triturando I/O. A partir do plano, a decisão fica objetiva: é falta de índice, é índice que o planner ignorou, é estatística desatualizada, ou é a query que precisa ser reescrita.

Na estratégia de índices, índice não é só CREATE INDEX. Usamos índices parciais quando a query sempre filtra um subconjunto (ex.: só pedidos `status = 'aberto'`), covering index com INCLUDE quando dá pra responder a query sem tocar na tabela, e BRIN para séries temporais grandes e ordenadas — onde um B-tree custaria gigabytes e o BRIN resolve com alguns megabytes. Cada índice novo é validado com o antes/depois do EXPLAIN, e índice redundante a gente remove, porque ele penaliza escrita sem pagar nada na leitura.

Quando o plano não resolve só com índice, partimos para reescrita de query e desnormalização cirúrgica: matar o N+1 com JOIN ou batch, trocar subquery correlacionada por LATERAL ou window function, materializar um agregado caro que é lido o tempo todo e escrito raramente. Desnormalizamos só o ponto que a medição justifica — não a esmo. E ajustamos a infra do banco em volta: connection pooling com PgBouncer (ou o pooler nativo do provedor) para parar de esgotar conexão; particionamento declarativo por data ou faixa quando a tabela já é grande demais para varrer inteira; autovacuum tuning para o bloat parar de degradar os planos; e réplica de leitura só quando o gargalo é realmente leitura concorrente — não como muleta pra esconder query ruim.

Stack que usamos em produção hoje

Banco: PostgreSQL é o default — é onde temos mais profundidade de tuning e onde o ferramental de diagnóstico (pg_stat_statements, auto_explain, EXPLAIN BUFFERS) é mais rico. MySQL e MariaDB quando é o que o cliente já roda em produção; aí trabalhamos com o slow query log, `EXPLAIN ANALYZE` e o performance_schema, e a mesma disciplina de medir antes de mexer se aplica igual.

Pooling e conexão: PgBouncer em modo transaction quando a aplicação abre muitas conexões curtas, ou o pooler gerenciado do provedor (RDS Proxy, pooler do Supabase, pooler do Coolify) quando já está na stack. O objetivo é o mesmo: desacoplar o número de workers da aplicação do número de conexões físicas no banco.

Diagnóstico e observability de query: EXPLAIN/EXPLAIN ANALYZE e auto_explain para os planos, pgbadger para transformar o log do Postgres em relatório navegável de queries lentas, e instrumentação de latência por query na camada da aplicação (OpenTelemetry ou o APM que o cliente já tem) para fechar o loop entre o que o usuário sente e o que o banco está fazendo. Deploy em Coolify quando o cliente já está nessa stack; AWS RDS/Aurora, Cloud SQL ou Azure Database quando o cliente exige cloud gerenciada específica.

O que vai (e o que não vai) entregar valor

Onde otimização de banco entrega valor real e rápido: uma query lenta recorrente que aparece no topo do pg_stat_statements e está pagando o preço de um índice ausente ou de um plano ruim; lock contention que trava transações em horário de pico; e custo de instância alto que é, na verdade, consequência de má modelagem ou N+1 — esses casos costumam render ganho grande mexendo em pouca coisa, porque o problema está concentrado em meia dúzia de queries.

Onde a gente vai ser honesto e dizer que tuning de banco não resolve: quando o gargalo é arquitetura da aplicação — chamada síncrona em série, falta de cache na camada certa, design que faz N requests onde deveria fazer um. Nenhum índice conserta uma aplicação que pergunta errado. E quando o volume genuinamente excede o que um único Postgres aguenta: aí a conversa é sharding, um motor colunar para analítico (ClickHouse, por exemplo) ou separar OLTP de OLAP — não mais um índice. A gente diz isso no diagnóstico, não depois de seis meses de tuning que não ia resolver.

A regra que usamos pra decidir: se as queries mais caras estão concentradas e o problema é plano de execução, índice ou modelagem local, otimização cabe e o retorno é alto. Se o problema está espalhado pela arquitetura do app ou o volume já passou do teto de um nó, a gente aponta isso primeiro — e só então conversa sobre o caminho certo, que pode não ser "otimizar o banco atual".

Propriedade · o resultado é seu

Não entregamos caixa-preta. O relatório de diagnóstico com os planos de execução, os scripts de migração de índice (com o rollback de cada um) e o benchmark de antes/depois ficam com você, versionados no seu repositório. Seu time consegue ler o que foi feito, reproduzir a medição e manter o ganho sem depender da gente. Conhecimento de performance que fica na sua casa, não conosco.

Próximo passo

O diagnóstico que abrimos é uma sessão técnica onde olhamos o pg_stat_statements (ou o slow query log) do seu banco e o EXPLAIN das queries mais caras, e respondemos uma pergunta só: dá pra resolver com índice, reescrita e pooling no banco atual, ou o problema é arquitetura/volume e exige outro caminho? Você sai com um ranking das queries que mais doem e a estimativa realista de ganho de cada correção. Sem POC eterno, sem proposta de 40 páginas.

Veja também

◆ Próximo passo

Diagnóstico de performance de banco

Olhamos o pg_stat_statements e o EXPLAIN das suas queries mais caras em 1 sessão técnica e dizemos o que resolve com índice, reescrita e pooling — sem subir a instância.