Tecnologias/02 - Bancos de Dados/01 - Relacionais/01 - PostgreSQL6 min
Arquitetura de Processos e Memória
Conceito
O Postgres é multiprocesso, não multithread. Essa é a decisão mais consequente da sua arquitetura, e ela é de 1986 — mantida porque isolamento de processo dá robustez: um backend que corrompe memória morre sozinho, sem derrubar o servidor.
Os processos
postmaster (pai)
├── backend ← UM POR CONEXÃO. executa suas queries.
├── backend
├── background writer ← empurra páginas sujas do buffer para o disco, aos poucos
├── checkpointer ← força TODAS as páginas sujas ao disco, periodicamente
├── WAL writer ← grava o WAL buffer em disco
├── autovacuum launcher → autovacuum worker (N) ← limpa tuplas mortas
├── stats collector / logger
└── walsender / walreceiver ← replicação
Quando um cliente conecta, o postmaster faz fork. O backend nasce com ~5–10 MB de memória privada e vive até a conexão fechar. Consequências, todas práticas:
- Conectar é caro (fork + inicialização de catálogo): ~1–3 ms, contra microssegundos de uma thread.
- Cada backend consome memória própria (
work_mempor operação, cache de catálogo, planos preparados). - Não há cap de "muitas conexões ociosas serem baratas": 500 conexões idle são 500 processos no scheduler do SO.
Daí a regra prática: max_connections alto não é generosidade, é risco. Ver
6. Particionamento e Pooling de Conexões.
A memória, em dois blocos
1. Memória compartilhada — alocada uma vez, na inicialização:
| Parâmetro | O que é | Ponto de partida |
|---|---|---|
shared_buffers |
cache de páginas de dados (8 KB cada) | 25% da RAM |
wal_buffers |
buffer do WAL antes de ir ao disco | auto (1/32 de shared_buffers) |
2. Memória por backend / por operação — multiplica pelo número de conexões:
| Parâmetro | O que é | Cuidado |
|---|---|---|
work_mem |
por operação de sort/hash, não por query | uma query com 4 sorts × 50 conexões = 200 × work_mem |
maintenance_work_mem |
VACUUM, CREATE INDEX, REINDEX |
pode ser generoso (poucos concorrentes) |
temp_buffers |
tabelas temporárias | por sessão |
E um parâmetro que não aloca nada:
effective_cache_size— apenas informa ao planejador quanta memória o SO provavelmente tem em cache. Valor alto (≈ 50–75% da RAM) faz o planejador confiar mais em index scan; valor baixo o empurra para seq scan. É puro palpite para o otimizador. Ver 5. Índices e o Planejador.
O caminho de uma página
query pede uma linha
↓
está em shared_buffers? ── sim → serve da RAM (buffer hit)
↓ não
está no page cache do SO? ─ sim → cópia rápida
↓ não
lê do disco
↓
UPDATE marca a página como "suja" (dirty)
↓
background writer escreve aos poucos / checkpointer força tudo
O Postgres depende do page cache do SO como segundo nível de cache — é por isso que
shared_buffers em 25% (e não 80%) costuma ser melhor: você deixa RAM para o SO cachear
os mesmos arquivos, sem dupla contabilidade.
Trade-offs
Processo dá robustez e custa escala de conexão. O MySQL usa thread por conexão e suporta milhares; o Postgres precisa de pooler antes disso. Em troca, um crash de backend no Postgres é isolado (o postmaster reinicia o conjunto se necessário, sem corromper).
work_mem é o parâmetro mais perigoso do arquivo. Ele parece "por conexão" e é por
nó de execução. Subir de 4 MB para 256 MB porque "temos RAM" é a receita clássica de
OOM: 100 conexões × 3 operações × 256 MB = 76 GB de teto teórico.
Ajustar por parâmetro global é grosseiro. A saída madura é work_mem conservador no
postgresql.conf e SET LOCAL work_mem = '256MB' na transação do relatório que
realmente precisa.
O trade-off central: o Postgres separa "memória compartilhada de cache" de "memória por operação", e só a primeira tem teto. Toda configuração de memória no Postgres é uma multiplicação implícita pelo número de conexões — e é por isso que limitar conexão é pré-requisito para ajustar memória.
Exemplo prático
Baseline defensável para uma máquina de 16 GB de RAM, 4 vCPU, SSD, carga OLTP:
# --- memória compartilhada ---
shared_buffers = 4GB # 25% da RAM
effective_cache_size = 12GB # só um palpite para o planejador
maintenance_work_mem = 1GB # VACUUM/CREATE INDEX mais rápidos
# --- memória por operação ---
work_mem = 16MB # conservador de propósito; suba por sessão
max_connections = 100 # com PgBouncer na frente
# --- WAL e checkpoint ---
wal_compression = on
max_wal_size = 4GB # checkpoints mais espaçados, menos I/O em pico
min_wal_size = 1GB
checkpoint_completion_target = 0.9 # espalha o I/O do checkpoint no tempo
# --- planejador em SSD ---
random_page_cost = 1.1 # padrão 4.0 assume disco mecânico!
effective_io_concurrency = 200
# --- autovacuum mais agressivo que o padrão ---
autovacuum_vacuum_scale_factor = 0.05 # padrão 0.2 = 20% da tabela morta
autovacuum_vacuum_cost_limit = 2000
autovacuum_naptime = 15s
# --- observabilidade ---
shared_preload_libraries = 'pg_stat_statements'
track_io_timing = on
log_min_duration_statement = 1000 # loga query > 1s
log_lock_waits = on
log_autovacuum_min_duration = 0 # veja o autovacuum trabalhando
random_page_cost = 4.0 é o padrão errado em 2026O valor default assume disco rotacional, onde acesso aleatório era 4× mais caro que sequencial. Em SSD/NVMe a diferença é pequena. Deixar 4.0 faz o planejador evitar índices e preferir varredura sequencial — é uma das causas mais comuns de "por que ele não usa meu índice?". Ver 5. Índices e o Planejador.
Inspecionar em produção
-- quem está conectado e fazendo o quê
SELECT pid, usename, state, wait_event_type, wait_event,
now() - query_start AS duracao, left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_type = 'client backend' AND state <> 'idle'
ORDER BY duracao DESC;
-- taxa de acerto do cache (queira > 0.99 em OLTP)
SELECT sum(blks_hit)::float / nullif(sum(blks_hit) + sum(blks_read), 0) AS cache_hit
FROM pg_stat_database;
-- conexões por estado: 'idle in transaction' é o inimigo (bloqueia VACUUM)
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY 2 DESC;
Como isso falha em produção
| Sintoma | Causa |
|---|---|
FATAL: sorry, too many clients already |
max_connections estourado — falta pooler (5. Go em Produção) |
| OOM killer matando o postmaster | work_mem × conexões × operações |
| Latência com picos periódicos | checkpoint escrevendo tudo de uma vez: suba max_wal_size e checkpoint_completion_target |
| CPU alta com pouca query | context switch de centenas de backends |
| Planejador ignorando índices | random_page_cost de disco mecânico; estatísticas velhas |
| Servidor lento após restart | shared_buffers frio; considere pg_prewarm |
Aprofundamento aplicado
Construir um orçamento de memória
Imagine 40 consultas simultâneas, cada uma com duas ordenações que cheguem a usar 32 MiB. Só essa conta ilustrativa alcança 2.560 MiB; ainda faltam buffers compartilhados, outros nós do plano, workers, conexões e memória do sistema operacional.
work_mem não é uma reserva única por conexão nem um teto total do servidor. Operações
de hash também podem usar o multiplicador correspondente. A concorrência efetiva e o
plano determinam como os consumos se somam.
Referência: parâmetros de memória.
Sintoma e experimento
Se um relatório grava temporários, aumentar memória só para essa sessão pode ser um experimento útil. Aumentar globalmente pode resolver um relatório e causar pressão de memória durante o pico de tráfego. Compare duração, temporários e memória sob concorrência.
Exercício: calcule dois cenários: 5 relatórios simultâneos e 50. Resultado esperado: perceba por que uma configuração que cabe no teste isolado pode exceder o orçamento real. Explicite quais parcelas a estimativa deixou de fora.
Relacionado
- 1. PostgreSQL
- 3. WAL, Checkpoint e Replicação
- 4. MVCC, Tuplas e VACUUM
- 6. Particionamento e Pooling de Conexões
- 7. Diagnóstico e Tuning
- 1. PostgreSQL vs MySQL vs Oracle
Parte de MOC PostgreSQL · Resource Consumption (docs)