Tuning de performance do PostgreSQL

PostgreSQL com configurações padrão é conservador, projetado para funcionar em qualquer hardware. Para produção, ajustar a memória, os índices e o autovacuum pode multiplicar a performance por 5-10x sem mudar uma linha do código da aplicação. A maioria dos problemas de performance vem de índices ausentes ou configurações de memória subdimensionadas.

Configurações de memória por tamanho de VPS

Parâmetros otimizados para diferentes tamanhos de servidor:

bash
# postgresql.conf — configurações por RAM disponível

# === VPS 2 GB RAM ===
shared_buffers = 512MB
effective_cache_size = 1536MB
work_mem = 16MB
maintenance_work_mem = 128MB
max_connections = 50

# === VPS 4 GB RAM ===
shared_buffers = 1GB
effective_cache_size = 3GB
work_mem = 32MB
maintenance_work_mem = 256MB
max_connections = 100

# === VPS 8 GB RAM ===
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 64MB
maintenance_work_mem = 512MB
max_connections = 200

# === Parâmetros comuns para todos os tamanhos ===
wal_buffers = 16MB
checkpoint_completion_target = 0.9
default_statistics_target = 100
random_page_cost = 1.1          # SSD: 1.1, HDD: 4.0
effective_io_concurrency = 200  # SSD: 200, HDD: 2

# Aplicar sem reiniciar (para parâmetros que suportam):
SELECT pg_reload_conf();

Identificar e criar índices ausentes

Queries sem índice causam seq scan, a principal causa de lentidão:

sql
-- Ver tabelas com muitos seq scans (candidatas a índice):
SELECT schemaname, tablename, seq_scan, seq_tup_read,
       idx_scan, idx_tup_fetch,
       n_live_tup AS linhas
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_scan DESC
LIMIT 20;

-- Ver índices que nunca são usados (candidatos a remoção):
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexrelname NOT LIKE 'pg_%'
ORDER BY tablename;

-- EXPLAIN ANALYZE para ver o plano de execução:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM pedidos
WHERE usuario_id = 123
  AND status = 'pendente'
ORDER BY criado_em DESC
LIMIT 10;

-- Se mostrar Seq Scan, criar índice:
CREATE INDEX CONCURRENTLY idx_pedidos_usuario_status
  ON pedidos (usuario_id, status, criado_em DESC);
-- CONCURRENTLY cria o índice sem bloquear a tabela

EXPLAIN ANALYZE: interpretar o plano

Leia os resultados do EXPLAIN ANALYZE para identificar gargalos:

sql
-- Executar e interpretar o plano:
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.nome, count(p.id) AS total_pedidos
FROM usuarios u
LEFT JOIN pedidos p ON p.usuario_id = u.id
WHERE u.criado_em > '2025-01-01'
GROUP BY u.id
ORDER BY total_pedidos DESC
LIMIT 100;

-- O que observar:
-- "Seq Scan" em tabelas grandes: falta índice
-- "actual time=X" alto: operação lenta
-- "rows=100 / actual rows=10000": estatísticas desatualizadas
--   → resolver com: ANALYZE nome_da_tabela;
-- "Buffers: shared hit=X read=Y": X = cache, Y = disco
--   → Y alto = falta memória (aumentar shared_buffers)
-- "Hash Join" vs "Nested Loop": Hash melhor para tabelas grandes

-- Usar pgMustard ou explain.dalibo.com para análise visual
-- (cole o resultado do EXPLAIN no site)

Configurar VACUUM e autovacuum

VACUUM evita table bloat e mantém estatísticas atualizadas para o planner:

sql
-- Verificar estado do autovacuum nas tabelas:
SELECT schemaname, relname,
       n_live_tup, n_dead_tup,
       last_autovacuum, last_autoanalyze,
       autovacuum_count, autoanalyze_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

-- Tabela com muitos dead tuples: forçar VACUUM manual
VACUUM ANALYZE pedidos;

-- Para tabelas com alta taxa de UPDATE/DELETE — ajustar autovacuum:
ALTER TABLE pedidos SET (
  autovacuum_vacuum_scale_factor = 0.01,    -- 1% (padrão: 20%)
  autovacuum_analyze_scale_factor = 0.005,  -- 0.5%
  autovacuum_vacuum_cost_delay = 2          -- ms (padrão: 2ms)
);

-- postgresql.conf — configurações globais de autovacuum
autovacuum_max_workers = 3
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
autovacuum_vacuum_cost_delay = 2ms

Queries lentas: log e análise com pg_stat_statements

Habilite pg_stat_statements para identificar as queries mais custosas:

sql
-- Habilitar pg_stat_statements (postgresql.conf):
-- shared_preload_libraries = 'pg_stat_statements'
-- pg_stat_statements.max = 10000
-- pg_stat_statements.track = all
-- Reiniciar: sudo systemctl restart postgresql

-- Criar extensão no banco:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Top 10 queries por tempo total:
SELECT round(total_exec_time::numeric / 1000, 2) AS total_seg,
       calls,
       round(mean_exec_time::numeric, 2) AS media_ms,
       round(stddev_exec_time::numeric, 2) AS stddev_ms,
       query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- Resetar estatísticas (após otimizações):
SELECT pg_stat_statements_reset();

-- Queries lentas no log (postgresql.conf):
log_min_duration_statement = 500   # logar queries > 500ms

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

shared_buffers é o cache de páginas do PostgreSQL em memória. Regra padrão: 25% da RAM. O OS também faz cache (effective_cache_size estima isso). Se shared_buffers estiver baixo, o banco lê frequentemente do disco. Após aumentar, monitore cache hit ratio: SELECT sum(blks_hit)/(sum(blks_hit)+sum(blks_read)) FROM pg_stat_database;, deve ser > 99%.

CONCURRENTLY: cria o índice sem bloquear leituras ou escritas na tabela, leva mais tempo (2 passes) mas não causa downtime. Índice normal: bloqueia escritas durante a criação, muito mais rápido mas inadequado para tabelas grandes em produção. Regra: sempre use CONCURRENTLY em produção.

Causas comuns: (1) tabela de produção tem muito mais dados (seq scan fica mais caro); (2) estatísticas desatualizadas (ANALYZE a tabela); (3) configurações de memória diferentes; (4) plano de query diferente por diferença de dados. Use EXPLAIN ANALYZE em produção para ver o plano real e comparar com desenvolvimento.

VACUUM regular não bloqueia leituras/escritas normais. VACUUM FULL bloqueia completamente e deve ser evitado em produção (use apenas em manutenção programada). autovacuum cuida do VACUUM automático, se estiver configurado corretamente, você raramente precisa rodar VACUUM manual.

Table bloat é o acúmulo de espaço não reutilizável causado por UPDATEs e DELETEs que não foram limpos pelo VACUUM. Uma tabela com muito bloat ocupa mais espaço em disco e leva mais tempo para seq scans. Evite com autovacuum bem configurado. Monitore com pgstattuple ou SELECT * FROM pgstattuple('tabela').