Voltar ao Blog
Databases & Backend25 de outubro de 202510 min de leitura

A Caixa de Ferramentas Escondida do Postgres: as Features que Sua Stack Provavelmente Já Substitui

DR

Doug Ribeiro

10 min de leitura

O post da escada de adoção cobriu as decisões macro: quais features do Postgres usar como base da sua stack e onde parar. Este post trata das decisões micro. Ferramentas dentro do Postgres que substituem, cada uma, um pedacinho da sua infraestrutura, e que ninguém coloca no tour habitual de features porque não são dramáticas o bastante pra puxar uma palestra de conferência.

A maioria dos engenheiros seniors que conheço adotou duas ou três dessas e não faz ideia que o resto existe. Pagam um serviço de scheduling porque nunca notaram o pg_cron. Sobem Redis porque ninguém contou deles sobre advisory locks. Não são features de 'Postgres faz tudo'. São features de 'Postgres já fazia isso e você não sabia'.

LISTEN/NOTIFY, usado do jeito certo

O post da escada colocou LISTEN/NOTIFY no Nível 3 porque os times tentam fazer dele o sistema nervoso de um produto de alto tráfego e se queimam. Esse veredito continua. Mas tem um caso de uso certo que merece respeito: coordenação de baixa frequência entre sua aplicação e seu banco.

Exemplos que funcionam bem: um dashboard admin em Angular que deve fazer auto-refresh quando um agente de suporte atualiza um ticket. Uma camada de cache que precisa invalidar uma chave específica quando uma linha muda. Um worker que acorda quando novos jobs aparecem (combinado com SKIP LOCKED, isso te dá queues event-driven sem polling).

Regras de bolso: abaixo de ~100 listeners, payloads abaixo de 1KB, writes por segundo nas centenas baixas. Essa é a zona confortável, não o teto. O post da escada coloca o teto prático perto de 1000 listeners, e a diferença entre os dois números é a folga que você quer ter no dia em que a carga de escrita disparar. Dentro da zona confortável é elegante, confiável e totalmente de graça. Fora dela, releia a escada.

Foreign Data Wrappers

postgres_fdw, mysql_fdw, mongo_fdw, file_fdw, sqlite_fdw. Dá pra consultar tabelas em outros bancos, arquivos CSV em disco, arquivos Parquet (via parquet_fdw, ou parquet_s3_fdw, um derivado que adiciona suporte a S3), como se fossem tabelas Postgres normais. Joins entre eles funcionam. Views em cima funcionam.

Esse é o que mais importa em migrações. Mover uma tabela MySQL legacy pro Postgres costumava envolver pipeline ETL, janela de cutover, e um sábado all-hands desconfortável. Com mysql_fdw, você cria a tabela foreign, escreve views que fazem join dos dados legacy com seu schema novo, migra incrementalmente, e derruba o FDW quando a última linha está local. Zero código ETL, sem pânico de cutover.

pg_cron

Jobs agendados, dentro do Postgres, definidos como SQL:

sql
SELECT cron.schedule(
  'cleanup-old-events',
  '0 2 * * *',
  $$DELETE FROM events WHERE created_at < NOW() - INTERVAL '90 days'$$
);

Esse é seu job de retenção inteiro. Sem DAG do Airflow, sem CronJob do Kubernetes, sem Lambda mais EventBridge. Se o que você precisa agendar é uma sentença SQL ou uma stored procedure, pg_cron basta.

É um eliminador genuíno de infraestrutura pra refrescar materialized views em agenda, expirar tokens, arquivar partições antigas, e fazer checks periódicos de qualidade de dados. No momento em que o trabalho agendado precisa de chamadas a APIs externas, você gradua pra fora. Até lá, isso economiza um serviço inteiro.

Advisory locks

Postgres tem dois sistemas de locking distintos. Um é o locking implícito de linha e tabela que você ganha das transações. O outro são locks definidos pela aplicação, chaveados por IDs inteiros arbitrários. Chame pg_advisory_lock(id) e você segura o lock até soltar ou a sessão acabar.

Isso substitui Redis pra locking cooperativo na maioria dos casos. 'Só um worker deveria rodar esse cron entre todas as instâncias' é um advisory lock num ID conhecido. 'Só um serviço deveria refrescar essa entrada de cache por vez' é um advisory lock chaveado num hash da chave de cache. 'Impedir que o mesmo usuário seja processado por dois workers ao mesmo tempo' é um advisory lock no user ID.

A pegadinha pra saber: advisory locks com escopo de sessão não funcionam com transaction pooling (PgBouncer em modo transaction). Use as variantes com escopo de transação (pg_advisory_xact_lock) se estiver atrás de um pooler.

Range types mais exclusion constraints

Problema duro clássico: duas pessoas não podem reservar o mesmo quarto de hotel em datas sobrepostas. A solução ingênua envolve um lock, um select, um check, um insert, e um loop de retry quando a race condition morde. A solução Postgres é uma constraint:

sql
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE bookings (
  room_id int,
  period tstzrange,
  EXCLUDE USING gist (room_id WITH =, period WITH &&)
);

A extensão btree_gist é o que permite ao GiST indexar o operador de igualdade em tipos escalares como int; sem ela, room_id WITH = não vai funcionar. Essa constraint torna sobreposições impossíveis no nível do banco. Sem race condition, sem lógica de retry, sem lock distribuído. Reservas, scheduling, alocação de recursos, qualquer problema de range temporal mapeia limpo pra esse padrão. O operador && checa sobreposição de ranges; a exclusion constraint proíbe dentro do mesmo room_id.

Já vi times construir microserviços inteiros pra lidar com isso com locks no Redis e bugs de correção, quando sete linhas de SQL teriam resolvido.

pg_stat_statements

Observabilidade de performance de queries embutida. Cada sentença que seu banco executa, agregada por formato (parâmetros removidos), com tempo total, tempo médio, contagem de chamadas e leituras de buffer. Adicione pg_stat_statements ao shared_preload_libraries no postgresql.conf, reinicie o Postgres, e então rode CREATE EXTENSION pg_stat_statements. O reinício é a parte que pega as pessoas: um CREATE EXTENSION puro sem o preload simplesmente não funciona.

É a primeira coisa que eu ligo em qualquer instância Postgres nova. Responde a pergunta mais comum, 'por que o banco está lento', sem um APM. Distributed tracing e contexto de app ainda precisam de um. Isso cobre o lado do banco e nada acima dele.

Combine com pg_stat_activity (queries ao vivo) e pg_stat_user_indexes (uso de índices), e você tem uma stack de observabilidade funcional pro banco com zero dependências externas.

Unlogged tables

Tabelas Postgres normais escrevem no WAL (write-ahead log) pra durabilidade. Unlogged tables não. Não são crash-safe (truncam em shutdown sujo), mas writes nelas são 2-3x mais rápidos e não pressionam replicação.

Casos que encaixam: storage de sessão, contadores de rate-limiting, métricas agregadas com retenção curta, buckets analíticos efêmeros. Qualquer workload 'formato Redis' onde perder dados num crash raro é aceitável.

Não são um substituto geral do Redis (sem semântica in-memory, sem pub/sub, sem suporte a TTL). Mas pra casos específicos onde você quer performance de escrita parecida com Redis sem rodar outro serviço, são uma opção legítima que ninguém lembra que existe.

Como Essas Features se Compõem

Cada uma isolada é uma pequena conveniência. A razão de importarem juntas é que cada uma que você adota é um serviço que você não roda. Sem instância de scheduler, sem cluster Redis pra locks, sem pipeline ETL, sem broker pub/sub separado, sem APM só pra ver queries lentas.

Esse é o argumento real pra 'just use Postgres', sem o hype: menos coisas pra monitorar, menos coisas pra atualizar, menos coisas pra te paginar às 3 da manhã. A escada te diz onde traçar a linha. A caixa de ferramentas te diz quanto terreno você cobre antes de chegar lá.

Referências

PostgreSQLDatabasesBackendTools