Performance Tuning PostgreSQL — Guia Completo 2026.
O PostgreSQL 17 chegou com melhorias sensíveis em vacuum, streaming replication e planner — mas o postgresql.conf default continua calibrado para rodar em qualquer lugar, não para o seu workload. Este guia condensa a metodologia de consultoria PostgreSQL que aplicamos em ambientes críticos: os seis pilares de tuning que respondem por 80% do ganho real de performance, com thresholds numéricos, exemplos práticos e os erros mais comuns que vemos em quem opera o Postgres sozinho.
A HTI opera bancos de dados relacionais desde 1990 — antes mesmo do PostgreSQL 1.0 existir — e aplica a disciplina de sustentação multi-RDBMS (MySQL, Oracle, SQL Server) a ambientes Postgres em produção. Cada threshold aqui foi validado contra incidente real, não contra documentação genérica da internet.
POR QUE O POSTGRESQL.CONF DEFAULT NÃO SERVE PARA PRODUÇÃO
O instalador do PostgreSQL não conhece seu hardware, seu padrão de acesso, nem seu SLA. shared_buffers default é 128MB. work_mem é 4MB. max_wal_size é 1GB. autovacuum_vacuum_scale_factor é 0.2 — ou seja, autovacuum só dispara quando 20% das linhas da tabela estão mortas. Isso é configuração para um Raspberry Pi de laboratório, não para um banco que sustenta receita.
O resultado prático: o Postgres "funciona" no primeiro dia e, seis meses depois — com mais dados, mais conexões, mais concorrência — começa a degradar de forma errática. Bloat cresce em silêncio, queries que rodavam em 20 ms passam a rodar em 2 s, checkpoints geram picos de I/O misteriosos. Quem opera o banco culpa a aplicação, quem faz a aplicação culpa o banco, e ninguém olha para o postgresql.conf que nunca foi tocado. Os seis pilares abaixo são a base do que ajustamos em qualquer Health Check de banco de dados.
shared_buffers é o cache de páginas do próprio PostgreSQL — dados e índices em RAM. Diferente do MySQL InnoDB (que quer 60–75% da RAM), o Postgres compartilha responsabilidade com o page cache do kernel Linux. O ponto de partida da comunidade é 25% da RAM em servidor dedicado, e raramente vale ultrapassar 40% — acima disso, você compete com o kernel e a curva de retorno inverte.
Thresholds práticos por perfil de servidor
Servidor dedicado 8 GB de RAM: shared_buffers = 2GB. 32 GB: 8GB. 128 GB: 16–32GB (não escala linearmente). Acima de 256 GB, avaliar caso a caso — o retorno marginal cai rápido. effective_cache_size deve refletir o total disponível (shared_buffers + OS page cache) e serve para o planner escolher entre index scan e seq scan; um alvo comum é 50–75% da RAM total.
Um erro clássico: aumentar shared_buffers para 80% da RAM "para o Postgres ter tudo em cache". Resultado: OOM killer derruba o processo, ou o sistema entra em swap e a latência explode. Postgres não é MySQL — o kernel Linux é parte do design de cache.
Ferramentas para dimensionamento
A extensão pg_buffercache mostra o que está de fato ocupando o shared_buffers — quais tabelas, quais índices, quantas páginas dirty. Rodar SELECT c.relname, count(*) AS buffers FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid) GROUP BY c.relname ORDER BY buffers DESC LIMIT 20; em produção revela se o cache está sendo consumido pelas tabelas certas ou por uma tabela de log que ninguém consulta.
"Shared_buffers no Postgres não é 'quanto mais melhor'. É 'quanto o suficiente para o working set caber, sem competir com o page cache do kernel'."
02
WORK_MEM E MAINTENANCE_WORK_MEM
work_mem é a memória disponível para operações internas de query — sort, hash join, hash aggregate — antes de "vazar" para arquivos temporários em disco. O default 4MB força praticamente qualquer ORDER BY ou JOIN em tabela média a usar disco. O problema é que work_mem é por operação, por conexão — 100 conexões concorrentes rodando queries com 3 sorts cada podem consumir 100 × 3 × work_mem.
Thresholds práticos
OLTP com muitas conexões (200+): work_mem entre 16MB e 32MB. Servidor com pool controlado (50–100 conexões) e workload analítico misto: 64MB a 128MB. DW / relatório com poucas conexões: 256MB a 1GB, aplicado por sessão via SET work_mem = '512MB' — nunca no default global. Sempre calibrar com EXPLAIN (ANALYZE, BUFFERS) e procurar Sort Method: external merge Disk — cada ocorrência é I/O evitável.
maintenance_work_mem
Usado por VACUUM, CREATE INDEX, ALTER TABLE e restore de dump. O default 64MB torna qualquer manutenção lenta em tabela grande. Ponto de partida em servidor de produção: 1GB a 2GB (não multiplica por conexão — poucas rodam simultaneamente). Para pg_restore ou REINDEX CONCURRENTLY em tabela de bilhões de linhas, subir temporariamente para 8GB ou 16GB reduz horas de janela.
03
AUTOVACUUM — O PROCESSO QUE EVITA QUE O BANCO PARE
PostgreSQL usa MVCC: todo UPDATE e DELETE deixa a linha antiga como dead tuple. O autovacuum limpa esses dead tuples e atualiza estatísticas do planner. Se autovacuum não acompanhar o ritmo de escrita, três coisas acontecem — nessa ordem: (1) bloat de tabela e índice cresce, degradando planos; (2) I/O explode quando o vacuum finalmente roda em massa; (3) no limite, você bate em transaction ID wraparound, o Postgres entra em modo somente-leitura de emergência e você tem um incidente de produção público.
Thresholds práticos por tipo de tabela
O default autovacuum_vacuum_scale_factor = 0.2 dispara autovacuum quando 20% da tabela está morta — inaceitável em tabelas grandes e write-heavy (uma tabela de 500 GB só é limpa quando acumula 100 GB de bloat). Ajuste por tabela, não no global:
No global (postgresql.conf) para servidor OLTP com escrita moderada a alta: autovacuum_max_workers = 6 (default 3), autovacuum_naptime = 15s (default 60s), autovacuum_vacuum_cost_limit = 1000 a 2000 (default 200), autovacuum_vacuum_cost_delay = 2ms (default 2ms — 20ms em versões antigas). Autovacuum "agressivo" bem calibrado gera pressão constante e previsível de I/O — não picos.
Sinais de que o autovacuum não está dando conta
dead_pct acima de 20% em tabelas quentes, last_autovacuum com horas ou dias de atraso, pg_stat_activity mostrando muitas queries autovacuum: to prevent wraparound (esse é o alerta vermelho — Postgres está fazendo vacuum emergencial), ou datfrozenxid se aproximando de 200 milhões (verifique com SELECT datname, age(datfrozenxid) FROM pg_database;). Alvo: manter age < 500M; limite crítico: 2 bilhões.
DIAGNÓSTICO COMPLETO
🔍 PostgreSQL Health Check.
Diagnóstico completo de performance, segurança e alta disponibilidade em PostgreSQL — entregue em até 5 dias úteis com plano de ação priorizado por severidade.
Solicitar Health Check →04
WAL E CHECKPOINT — ESCRITAS QUE TRAVAM SOB PICO
Toda escrita no PostgreSQL passa primeiro pelo WAL (Write-Ahead Log). Checkpoint é o processo que garante que páginas modificadas em shared_buffers foram efetivamente escritas nos datafiles — para que o WAL correspondente possa ser reciclado. Um max_wal_size pequeno demais força checkpoints frequentes; um checkpoint_completion_target mal ajustado concentra I/O de escrita em picos, gerando latência intermitente que a aplicação percebe como "o banco fica lento a cada 5 minutos".
Thresholds práticos
Workload OLTP moderado: max_wal_size = 4GB, min_wal_size = 1GB, checkpoint_timeout = 15min, checkpoint_completion_target = 0.9. Workload write-heavy (fintech, e-commerce, ingestão): max_wal_size = 16GB a 32GB, checkpoint_timeout = 30min. Objetivo prático: manter checkpoints_req / (checkpoints_timed + checkpoints_req) < 5%. Acima disso, checkpoint por volume de WAL está ganhando de checkpoint por tempo — sinal claro de max_wal_size baixo.
wal_buffers geralmente auto-ajusta bem (default -1 = 1/32 do shared_buffers, teto 16MB). Em workload de commit muito alto (10k+ TPS), fixar em 64MB reduz contenção. wal_compression = on (LZ4 no 15+) reduz volume de WAL em 30–60% ao custo de CPU marginal — quase sempre vale ligar em ambiente com replicação ou WAL archiving.
synchronous_commit — o trade-off de durabilidade
synchronous_commit = on (default): cada commit espera fsync no WAL. Seguro, mais lento. = off: perde até wal_writer_delay (default 200ms) de transações em crash de host, mas dados no disco não corrompem. = local: espera local mas não replica síncrona. Mexer nesse parâmetro em produção sem entender o RPO contratual é uma das causas mais comuns de perda de dados em incidente.
05
PG_STAT_STATEMENTS — A FONTE DE VERDADE
Tuning sem evidência é chute. pg_stat_statements agrega toda query executada por fingerprint — tempo total, tempo médio, I/O, número de execuções. Não vem habilitada por default e ainda vemos muitos ambientes rodando sem ela. Instalar exige uma linha no postgresql.conf e restart.
É comum descobrir que 5 queries consomem 80% do tempo total do banco — e três delas têm correção trivial (índice ausente, função em coluna do WHERE, SELECT * em tabela wide). Complemente com EXPLAIN (ANALYZE, BUFFERS) para cada uma: Buffers: shared read= alto indica I/O evitável; Rows Removed by Filter alto indica índice faltando ou ordem errada de colunas.
Índices no PostgreSQL — além do B-tree
B-tree resolve 90% dos casos. Mas Postgres oferece: GIN para busca full-text e jsonb, GiST para dados geoespaciais (PostGIS) e ranges, BRIN para tabelas gigantescas com dados naturalmente ordenados (logs, séries temporais) — BRIN em tabela de 1 TB pode ocupar 8 MB no lugar de 50 GB de B-tree. Índices parciais (CREATE INDEX ... WHERE status = 'active') são especialmente subutilizados: reduzem tamanho, aceleram writes e ainda cobrem a query que importa.
Índices duplicados e nunca usados degradam writes sem retorno. Auditoria trimestral com SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC; costuma liberar 10–30% do storage em ambientes maduros.
06
CONNECTION POOLING — O GARGALO QUE O POSTGRES NÃO RESOLVE SOZINHO
Cada conexão no PostgreSQL cria um processo do sistema operacional (diferente do MySQL, que usa threads). Cada processo consome 8–15 MB de RAM base + work_mem por operação. Em aplicação com muitas conexões simultâneas (serverless, microsserviços, Lambdas), max_connections = 500 vira 4–8 GB de RAM só de conexões ociosas antes de qualquer query rodar. Aumentar max_connections sem connection pooler é o caminho mais curto para OOM.
PgBouncer — praticamente obrigatório
PgBouncer em modo transaction pooling é o padrão para aplicações com alta concorrência. Uma configuração típica: pool_mode = transaction, default_pool_size = 25, max_client_conn = 2000. Resultado: a aplicação abre 2000 conexões contra o PgBouncer, que sustenta apenas 25 conexões efetivas contra o Postgres — memória sob controle e latência de acquire próxima de zero.
Ressalva importante: modo transaction desabilita SET de sessão, cursores nomeados e LISTEN/NOTIFY. Frameworks modernos (Rails, Django, Node, Java com HikariCP) lidam bem, mas exigem revisão. Em RDS, use o RDS Proxy; em Supabase, o pooler embutido faz o mesmo. Em Aurora, avaliar Aurora Serverless v2 + pooler externo — Aurora Proxy não substitui PgBouncer em todos os cenários.
Combine com idle_in_transaction_session_timeout = '5min' e statement_timeout por role (relatório 5min, OLTP 30s) para evitar que uma transação esquecida ou query fugida trave conexão indefinidamente. Em ambientes com SLA 24/7, esse ajuste é rotina no escopo de DBA Remoto PostgreSQL.
07
CHECKLIST FINAL DE PERFORMANCE TUNING POSTGRESQL (2026)
Use isto como base — não como dogma. Cada ambiente tem trade-offs próprios:
- Versão em PostgreSQL 16 ou 17 (ou 15 dentro do ciclo de suporte até 2027).
shared_buffersem 25% da RAM, ajustado por cache hit ratio > 99%.effective_cache_sizeem 50–75% da RAM total.work_memcalibrado por pico de conexões — nunca "chute" global alto.maintenance_work_memem 1–2 GB para acelerar VACUUM, CREATE INDEX e restore.- Autovacuum tunado por tabela em tabelas write-heavy (scale_factor 0.02–0.05).
autovacuum_vacuum_cost_limitentre 1000 e 2000;autovacuum_max_workers≥ 6.- Zero tabela com
dead_pct > 20%;age(datfrozenxid) < 500M. max_wal_size≥ 4 GB (OLTP) ou ≥ 16 GB (write-heavy).checkpoint_completion_target = 0.9;req_pct < 5%.wal_compression = onquando há replicação ou WAL archiving.pg_stat_statementsativo e consultado semanalmente.- Top 20 queries do
pg_stat_statementsauditadas a cada release. pg_stat_user_indexesrevisado trimestralmente — índicesidx_scan = 0removidos.- PgBouncer (ou RDS Proxy / pooler equivalente) em
transaction pooling. idle_in_transaction_session_timeoutestatement_timeoutconfigurados por role.- Backup com restore testado em ambiente isolado nos últimos 90 dias.
- Monitoramento ativo com plano de resposta — interno ou via DBA Remoto PostgreSQL.
08
QUANDO CHAMAR UM DBA ESPECIALISTA
Performance tuning de PostgreSQL tem um teto claro para quem opera o banco sozinho: o ponto em que cada hora de investigação custa mais do que o resultado entrega. Esse momento chega cedo em três cenários:
- Incidente em produção agora: banco lento, conexões estourando, bloat descontrolado, sinal de wraparound. Aqui o tempo de resposta vale mais que o método — é caso de atendimento emergencial 24/7.
- Crescimento previsível batendo no teto: a aplicação dobrou de tráfego em 12 meses, e os mesmos parâmetros não escalam. É hora de auditoria estruturada + plano de capacidade — escopo típico de consultoria PostgreSQL.
- Alta disponibilidade exigida por contrato: SLA de 99,9% ou mais não se sustenta com tuning pontual; exige Streaming Replication, Patroni, failover testado e monitoramento contínuo. Escopo natural de DBA Remoto PostgreSQL.
A HTI opera bancos relacionais desde 1990, sustenta mais de 500 clientes em produção crítica e aplica ao PostgreSQL o mesmo padrão operacional que sustenta SLAs de 99,9999% em ambientes MySQL, Oracle e SQL Server. Não vendemos hora de tentativa: vendemos diagnóstico e correção baseados em recorrência multi-RDBMS.
PRÓXIMO PASSO
Precisa de ajuda para aplicar isso em produção?
Os seis pilares acima são o ponto de partida. Aplicá-los em produção sem janela de risco exige método, monitoramento e quem já viu o mesmo padrão de falha antes — em Postgres, em Oracle, em MySQL.
09
PERGUNTAS FREQUENTES
Qual o valor ideal de shared_buffers em produção?
Em servidor PostgreSQL dedicado, 25% da RAM é o ponto de partida clássico da comunidade — não a resposta final. Acima de 40% raramente traz ganho porque o kernel do Linux já mantém page cache eficiente e você passa a competir com ele. O valor correto depende do working set: se o índice + tabelas quentes cabem em shared_buffers, você elimina I/O para essas páginas. Em ambientes com mais de 128 GB de RAM, testar valores entre 8 e 32 GB e medir cache hit ratio via pg_stat_database é mais confiável do que aplicar a regra dos 25% no escuro.
Autovacuum agressivo pode derrubar a produção?
Sim — mas autovacuum tímido derruba primeiro. O default vacuum_cost_limit=200 é conservador demais para tabelas write-heavy modernas; bloat acumula, planos degradam e no limite você chega em transaction ID wraparound (o banco para de aceitar escrita). O caminho correto é tunar por tabela: autovacuum_vacuum_scale_factor entre 0.02 e 0.05 em tabelas quentes, autovacuum_vacuum_cost_limit em 1000–2000 e autovacuum_max_workers alinhado à quantidade de núcleos. Autovacuum bem calibrado gera pressão constante e previsível de I/O, não picos.
Posso aplicar este guia em RDS PostgreSQL, Aurora e Supabase?
Sim, com ressalvas. Em RDS PostgreSQL você ajusta tudo via parameter group; em Aurora PostgreSQL-compatible, shared_buffers, WAL e checkpoint têm comportamento próprio (storage distribuído) e vários parâmetros são fixos. Em Supabase, você tem acesso ao parameter tuning via dashboard/CLI, mas o Postgres roda em pooler compartilhado — connection pooling é responsabilidade nossa. Em qualquer plataforma gerenciada, pg_stat_statements, autovacuum tuning e revisão de índices funcionam igual ao on-premises.
Vale a pena migrar de PostgreSQL 15 para 17 em 2026?
Na maioria dos casos, sim. PostgreSQL 17 traz melhorias reais em vacuum (menos memória, mais rápido), streaming replication mais eficiente, planner com melhor tratamento de subplans e ganhos de performance em COPY. 16 continua em suporte, e 15 está em suporte até novembro de 2027 — não há pressa emergencial, mas todo upgrade adiado vira dívida técnica. Faça em janela planejada, com pg_upgrade em ambiente espelhado antes.