Tecnologias/02 - Bancos de Dados/01 - Relacionais/01 - PostgreSQL6 min
Diagnóstico e Tuning
Conceito
O Postgres não tem AWR como o Oracle. Ele tem algo diferente: um conjunto de views de estatística acumulada e um sistema de wait events, que juntos respondem a pergunta que importa num incidente:
O banco está esperando o quê?
Toda investigação segue a mesma ordem. Vale decorar:
1. Está esperando? → pg_stat_activity (wait_event_type)
2. Está bloqueado? → pg_locks / pg_blocking_pids()
3. Qual query custa mais? → pg_stat_statements
4. Por que essa query? → EXPLAIN (ANALYZE, BUFFERS)
5. É manutenção? → pg_stat_user_tables (dead_tup, last_autovacuum)
Wait events: a taxonomia
pg_stat_activity.wait_event_type diz a categoria do bloqueio, e ela aponta direto
para a causa raiz:
wait_event_type |
Significa | Onde investigar |
|---|---|---|
Lock |
esperando lock lógico (linha, tabela) | contenção da aplicação, DDL, transação longa |
LWLock |
lock interno leve (buffer, WAL) | contenção do próprio Postgres; muitas conexões |
IO |
lendo/escrevendo disco | shared_buffers, disco, query varrendo demais |
Client |
esperando o cliente | app lento lendo resultado, ou idle in transaction |
IPC |
esperando outro processo (paralelismo, sync rep) | réplica síncrona, workers paralelos |
Timeout |
espera programada | vacuum delay, etc. |
(null) + active |
usando CPU | é a query mesmo; otimize o plano |
Um wait_event_type = 'Client' com state = 'idle in transaction' é o achado mais
valioso: significa que a aplicação abriu transação e foi fazer outra coisa — e isso
bloqueia VACUUM (4. MVCC, Tuplas e VACUUM) e segura locks.
pg_stat_statements: por onde o tempo vai
É a extensão mais importante do Postgres. Ela normaliza as queries (substitui literais por
$1) e acumula tempo, chamadas, I/O e uso de cache por query id.
CREATE EXTENSION pg_stat_statements; -- + shared_preload_libraries
-- o ranking que importa: tempo TOTAL, não tempo médio
SELECT round(total_exec_time::numeric / 1000, 1) AS total_s,
calls,
round(mean_exec_time::numeric, 2) AS media_ms,
round(100.0 * shared_blks_hit /
nullif(shared_blks_hit + shared_blks_read, 0), 1) AS cache_pct,
rows / nullif(calls, 0) AS linhas_por_call,
left(query, 80)
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 15;
Uma query de 5 ms chamada 2 milhões de vezes consome muito mais que uma de 8 s chamada
3 vezes — e é muito mais fácil de corrigir. Otimizar por mean_exec_time faz você
gastar o dia no relatório mensal enquanto o 7. Problema N+1 consome 60% do banco.
Locks: quem está bloqueando quem
-- a árvore de bloqueio, direto ao ponto
SELECT bloqueado.pid AS pid_bloqueado,
bloqueado.query AS query_bloqueada,
bloqueador.pid AS pid_bloqueador,
bloqueador.query AS query_bloqueadora,
bloqueador.state AS estado_bloqueador,
now() - bloqueado.query_start AS esperando_ha
FROM pg_stat_activity bloqueado
JOIN pg_stat_activity bloqueador
ON bloqueador.pid = ANY(pg_blocking_pids(bloqueado.pid))
WHERE cardinality(pg_blocking_pids(bloqueado.pid)) > 0;
O padrão de incidente mais comum aparece assim: um ALTER TABLE esperando lock, e 300
SELECT esperando atrás dele. O Postgres enfileira locks de forma justa (FIFO), então um
DDL que espera bloqueia tudo que chega depois, inclusive leituras que seriam
compatíveis entre si.
-- a defesa, sempre, antes de DDL:
SET lock_timeout = '3s';
ALTER TABLE grande ADD COLUMN x int; -- falha rápido em vez de travar o serviço
Trade-offs
Observabilidade custa alguma performance. track_io_timing = on,
log_min_duration_statement baixo, auto_explain e pg_stat_statements com track = all
têm overhead (pequeno, mas real). O overhead de não ter isso durante um incidente é
muito maior — ligue todos, e apenas evite log_statement = 'all' em banco de alto volume.
Tuning de parâmetro raramente é a solução. A ordem de retorno sobre esforço é quase
sempre: query/índice/schema ≫ manutenção (vacuum/estatística) ≫ parâmetros de
memória ≫ hardware. Times gastam semanas em postgresql.conf quando um índice
parcial resolveria em cinco minutos (5. Índices e o Planejador).
Reset de estatística apaga histórico. pg_stat_statements_reset() é útil para medir
uma janela específica, e destrói a base de comparação. Colete em série temporal
(Prometheus + postgres_exporter, pgwatch, pganalyze) em vez de olhar snapshots.
O trade-off central: o Postgres oferece dados crus e nenhuma conclusão. Isso o torna honesto e exige método: sem a sequência espera → bloqueio → query → plano, você fica mexendo em parâmetros no escuro.
Exemplo prático
Baseline de observabilidade
shared_preload_libraries = 'pg_stat_statements,auto_explain'
pg_stat_statements.max = 10000
pg_stat_statements.track = all # inclui statements dentro de funções
track_io_timing = on
track_functions = pl
log_min_duration_statement = 1000 # loga query acima de 1s
log_lock_waits = on # loga quem espera lock > deadlock_timeout
log_autovacuum_min_duration = 0 # veja todo autovacuum
log_temp_files = 0 # denuncia work_mem insuficiente
log_checkpoints = on
log_line_prefix = '%m [%p] %u@%d app=%a ' # aplicação no log = ouro em incidente
auto_explain.log_min_duration = 3000 # plano completo de queries > 3s
auto_explain.log_analyze = on
auto_explain.log_buffers = on
Playbook: "o banco está lento agora"
-- 1. panorama: quantos ativos, esperando o quê
SELECT state, wait_event_type, wait_event, count(*)
FROM pg_stat_activity WHERE backend_type = 'client backend'
GROUP BY 1,2,3 ORDER BY 4 DESC;
-- 2. as 10 queries mais longas rodando agora
SELECT pid, now() - query_start AS duracao, wait_event_type, wait_event,
left(query, 100)
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend'
ORDER BY query_start LIMIT 10;
-- 3. tem bloqueio? (query da seção anterior)
-- 4. matar o culpado, se necessário
SELECT pg_cancel_backend(12345); -- tenta cancelar a query (educado)
SELECT pg_terminate_backend(12345);-- mata a conexão (último recurso)
As 8 métricas de dashboard
| Métrica | Fonte | Alerta quando |
|---|---|---|
| Cache hit ratio | pg_stat_database |
< 0,95 em OLTP |
| Conexões por estado | pg_stat_activity |
idle in transaction > 0 por > 1 min |
| Transação mais antiga | pg_stat_activity |
> 5 min |
| Tuplas mortas por tabela | pg_stat_user_tables |
> 20% e sem last_autovacuum recente |
| Idade do XID | pg_class.relfrozenxid |
> 50% de autovacuum_freeze_max_age |
| Lag de replicação | pg_stat_replication |
crescendo (derivada!) |
| WAL retido por slot | pg_replication_slots |
> limiar de disco |
| Deadlocks / conflitos | pg_stat_database |
qualquer crescimento |
Lag de 200 MB estável é saudável. Lag de 20 MB crescendo há 10 minutos é um incidente. O mesmo vale para tuplas mortas, WAL retido e tamanho de fila. Ver Monitoring.
Como isso falha em produção
| Sintoma | Diagnóstico |
|---|---|
| "Lento" sem query lenta | espera em LWLock/IO: conexões demais, ou disco saturado |
| Tudo travado de repente | um DDL esperando lock, com toda a carga na fila atrás |
| Lento só de manhã | job de relatório concorrendo; autovacuum represado da noite |
| Piorou após deploy | plano novo, índice faltando, ou N+1 novo no ORM |
| Piorou sem deploy | dado cresceu e mudou o regime do plano; estatística velha |
temp file no log |
work_mem insuficiente para o sort/hash |
| Nada explica | mede-se pouco: o baseline acima não estava ligado |
Aprofundamento aplicado
Separar causa imediata de causa que iniciou a fila
Uma consulta que normalmente leva 5 ms pode esperar 30 s por um lock. Otimizar seu índice não remove a transação bloqueadora. Comece localizando o tipo de espera e a cadeia de bloqueios; depois examine por que o bloqueador permanece aberto.
| Observação | Próxima investigação |
|---|---|
| Muitos clientes esperando conexão | Pool, duração das consultas e limite de concorrência |
| Sessões esperando lock | Bloqueador, transação e ordem de atualização |
| Consulta lê muito e retorna pouco | Índice, filtro e estimativas |
| Muitas chamadas rápidas do mesmo SQL | Padrão N+1 e chamadas evitáveis |
Exercício: crie uma sessão que atualiza uma linha e não confirma; faça outra tentar atualizá-la. Resultado esperado: identifique a primeira sessão como bloqueadora, mesmo que ela apareça sem executar SQL naquele instante. Só encerre sessões de estudo.
Uma boa comparação registra consulta, parâmetros, volume, concorrência e janela de medição. Comparar tempo com cache quente a tempo com cache frio confunde a conclusão. Referência: estatísticas e eventos de espera.
Relacionado
- 5. Índices e o Planejador
- 4. MVCC, Tuplas e VACUUM
- 2. Arquitetura de Processos e Memória
- 6. Particionamento e Pooling de Conexões
- SQL Tuning
- Monitoring
- 4. Otimizador, Planos e Bind Variables
Parte de MOC PostgreSQL · Monitoring (docs)