LGMdevs
Voltar ao blog
Equipe LGMdevs

Postgres que aguenta: índices, N+1 e connection pooling

A maioria dos bancos lentos não precisa de uma máquina maior — precisa de três correções baratas. Onde a lentidão realmente nasce e como resolvê-la.

BackendPostgreSQLPerformance

Quando um produto começa a arrastar sob carga, a reação comum é aumentar a máquina do banco. Às vezes é isso mesmo. Na maioria das vezes que investigamos, o gargalo é um de três problemas baratos de corrigir — e uma máquina maior só adia a conta. São eles: falta de índice, o problema N+1 e ausência de connection pooling.

1. O índice que faltava

Sem um índice, uma busca por WHERE email = ... obriga o Postgres a ler a tabela inteira linha por linha (sequential scan). Com 500 linhas, ninguém percebe. Com 5 milhões, cada consulta varre 5 milhões de linhas.

A ferramenta para enxergar isso é o EXPLAIN ANALYZE, que mostra o plano real e o tempo gasto:

EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 42;
-- Seq Scan on pedidos ... rows=5000000   ← lendo tudo

Um índice na coluna filtrada troca a varredura por uma busca direta:

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
-- Agora: Index Scan using idx_pedidos_cliente ... ← vai direto ao ponto

Dois cuidados que separam quem sabe usar índice de quem sai criando índice em tudo:

  • Índice não é grátis. Cada índice deixa escritas (INSERT/UPDATE) mais lentas e ocupa disco. Indexe as colunas que você filtra e ordena de fato, não todas.
  • Em índice composto, a ordem das colunas importa. Um índice em (cliente_id, data) serve para filtrar por cliente_id sozinho ou por cliente_id e data juntos — mas não para filtrar só por data. Ordene as colunas do filtro mais frequente para o menos.

2. O problema N+1

Este é o mais silencioso, porque cada consulta individual é rápida — o problema é a quantidade delas. Acontece quando você busca uma lista e, para cada item, dispara outra consulta:

const pedidos = await db.pedidos.findMany()      // 1 consulta
for (const pedido of pedidos) {
  pedido.cliente = await db.clientes.find(pedido.clienteId)  // + N consultas
}

Cem pedidos viram 101 idas ao banco. Cada uma some no log; juntas, elas dominam o tempo de resposta. A latência de rede entre app e banco, repetida N vezes, é o que mata.

A correção é buscar tudo de uma vez — com um JOIN, ou carregando os relacionados em um único IN:

// Uma consulta, com os clientes já embutidos
const pedidos = await db.pedidos.findMany({ include: { cliente: true } })

O jeito de flagrar N+1 é observar o número de queries por request, não só a duração de cada uma. Uma rota que dispara centenas de consultas quase idênticas é o sintoma.

3. Connection pooling

Abrir uma conexão com o Postgres é caro: há handshake, autenticação e alocação de memória no servidor. Um app que abre e fecha uma conexão por request desperdiça esse custo o tempo todo — e o Postgres tem um teto de conexões simultâneas (max_connections, tipicamente na casa das centenas). Estoure esse teto e novas requisições passam a receber erro de conexão, não lentidão.

Um pool mantém um conjunto de conexões abertas e reaproveitadas:

import { Pool } from "pg"

const pool = new Pool({ max: 10 })   // reaproveita até 10 conexões
const { rows } = await pool.query("SELECT * FROM pedidos WHERE id = $1", [id])

Dois pontos que costumam pegar em produção:

  • Serverless multiplica conexões. Cada instância serverless mantém o próprio pool; sob pico, dezenas de instâncias somam conexões e estouram o max_connections. Nesses ambientes, use um pooler externo (PgBouncer, ou o pooler gerenciado do seu provedor) entre o app e o banco.
  • Dimensione o pool com consciência. Pool grande demais só empurra a disputa para dentro do Postgres. O tamanho certo depende dos núcleos e do disco do banco — meça, não chute.

O diagnóstico antes do upgrade

Antes de pagar por uma instância maior, faça o barato primeiro: rode EXPLAIN ANALYZE nas consultas lentas, conte quantas queries cada request dispara e confirme que existe um pool com tamanho sensato. Na maior parte dos casos, o ganho vem daqui — e a máquina que você já tem passa a aguentar bem mais do que parecia.

Há um limite honesto: quando os três estão resolvidos e a carga ainda excede o hardware, aí sim escalar (réplicas de leitura, particionamento, mais máquina) é a resposta certa. A ordem é que importa — otimizar antes de escalar, não o contrário.


Tem um banco que trava sob carga e você não sabe por quê? Fale com a gente.

Fontes