BANCO DE DADOS · POSTGRESQL

    Performance Tuning PostgreSQL — Guia Completo 2026.

    HTI Tecnologia · Equipe TécnicaPublicado em julho de 202612–15 min de leitura

    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.

    01

    SHARED_BUFFERS E EFFECTIVE_CACHE_SIZE

    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.

    -- Cache hit ratio do PostgreSQL (alvo: > 99%)
    SELECT
      datname,
      ROUND(
        100.0 * sum(blks_hit) /
        NULLIF(sum(blks_hit) + sum(blks_read), 0),
        2
      ) AS cache_hit_ratio_pct
    FROM pg_stat_database
    WHERE datname NOT IN ('template0','template1','postgres')
    GROUP BY datname
    ORDER BY cache_hit_ratio_pct;

    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.

    -- Diagnóstico: quantas queries estão vazando para disco
    SELECT
      datname,
      temp_files,
      pg_size_pretty(temp_bytes) AS temp_size
    FROM pg_stat_database
    WHERE temp_files > 0
    ORDER BY temp_bytes DESC;
    -- Complementar: log_temp_files = 0 no postgresql.conf
    -- registra toda vez que uma query cria arquivo temp

    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.

    -- Bloat e status do autovacuum por tabela
    SELECT
      schemaname, relname,
      n_live_tup, n_dead_tup,
      ROUND(100.0 * n_dead_tup /
        NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
      last_autovacuum, autovacuum_count
    FROM pg_stat_user_tables
    WHERE n_dead_tup > 10000
    ORDER BY dead_pct DESC NULLS LAST
    LIMIT 20;

    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:

    -- Tuning por tabela para workload write-heavy
    ALTER TABLE pedidos SET (
      autovacuum_vacuum_scale_factor = 0.02,
      autovacuum_vacuum_threshold = 1000,
      autovacuum_analyze_scale_factor = 0.01,
      autovacuum_vacuum_cost_limit = 2000
    );

    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".

    -- Frequência e tipo de checkpoint (timed vs requested)
    SELECT
      checkpoints_timed,
      checkpoints_req,
      ROUND(100.0 * checkpoints_req /
        NULLIF(checkpoints_timed + checkpoints_req, 0), 2)
        AS req_pct,
      checkpoint_write_time, checkpoint_sync_time,
      buffers_checkpoint, buffers_backend
    FROM pg_stat_bgwriter;
    -- req_pct > 5% = max_wal_size subdimensionado
    -- buffers_backend alto = backends escrevendo (ruim)

    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.

    -- Ativação (postgresql.conf) + criar extensão
    shared_preload_libraries = 'pg_stat_statements'
    pg_stat_statements.max = 10000
    pg_stat_statements.track = all
    -- Após restart, por database:
    CREATE EXTENSION pg_stat_statements;
    -- Top 20 queries por tempo total (o alvo do tuning)
    SELECT
      ROUND(total_exec_time::numeric, 2) AS total_ms,
      calls,
      ROUND(mean_exec_time::numeric, 2) AS mean_ms,
      ROUND(100.0 * total_exec_time /
        SUM(total_exec_time) OVER (), 2) AS pct_total,
      LEFT(query, 120) AS query_preview
    FROM pg_stat_statements
    ORDER BY total_exec_time DESC
    LIMIT 20;

    É 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.

    -- Diagnóstico antes de mexer em max_connections
    SELECT
      state, count(*)
    FROM pg_stat_activity
    WHERE backend_type = 'client backend'
    GROUP BY state
    ORDER BY count DESC;
    -- idle in transaction > 5 é bandeira vermelha
    -- idle alto = pool da aplicação sem timeout

    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_buffers em 25% da RAM, ajustado por cache hit ratio > 99%.
    • effective_cache_size em 50–75% da RAM total.
    • work_mem calibrado por pico de conexões — nunca "chute" global alto.
    • maintenance_work_mem em 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_limit entre 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 = on quando há replicação ou WAL archiving.
    • pg_stat_statements ativo e consultado semanalmente.
    • Top 20 queries do pg_stat_statements auditadas a cada release.
    • pg_stat_user_indexes revisado trimestralmente — índices idx_scan = 0 removidos.
    • PgBouncer (ou RDS Proxy / pooler equivalente) em transaction pooling.
    • idle_in_transaction_session_timeout e statement_timeout configurados 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.