PostgreSQL é o banco de dados relacional mais avançado do mundo open-source, mas a maioria dos desenvolvedores usa menos de 20% de suas capacidades. Queries lentas, índices mal configurados e uso ingênuo de JSONB são as causas mais comuns de problemas de performance em aplicações que cresceram além do MVP. Este guia cobre as ferramentas que transformam um Postgres lento em uma máquina de queries.
EXPLAIN ANALYZE: a única ferramenta que importa
Antes de otimizar qualquer coisa, entenda o que está acontecendo. EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) mostra o plano de execução real da query, com tempo de cada nó, linhas estimadas vs reais, e acertos no cache de buffers. Sequential Scan em uma tabela de 10 milhões de linhas quando você esperava Index Scan é o sinal mais óbvio de um índice faltando. Nest Loop com muitas iterações em joins indica que as estatísticas de tabela podem estar desatualizadas — um simples ANALYZE tabela resolve.
Extensions como pg_stat_statements capturam estatísticas de todas as queries executadas: total de execuções, tempo médio, tempo total acumulado, e rows retornadas. A query que você não sabia que rodava 50.000 vezes por minuto aparece ali. auto_explain loga automaticamente o EXPLAIN ANALYZE de queries que ultrapassam um threshold de tempo — identifica queries problemáticas em produção sem instrumentação manual. Com esses dois dados, você sabe exatamente onde focar otimização.
Índices que você não está usando (mas deveria)
B-tree é o índice padrão e funciona para a maioria dos casos — comparações de igualdade, ranges, ORDER BY, e LIKE com prefixo fixo. Mas Postgres tem índices especializados para casos específicos: GIN para arrays, JSONB e full-text search (múltiplos valores por linha); GiST para dados geométricos, geoespaciais e exclusão de overlapping ranges; BRIN para tabelas enormes com dados naturalmente ordenados (logs por timestamp, eventos por ID sequencial — índice de 100KB cobre 100GB de dados). Hash indexes são opção quando você só precisa de igualdade exata e coluna de alta cardinalidade.
Partial indexes cobrem apenas linhas que satisfazem uma condição: CREATE INDEX ON pedidos (status) WHERE status != 'entregue'. Se 99% dos pedidos têm status ‘entregue’, o índice partial é 100x menor e muito mais rápido para queries em pedidos pendentes. Expression indexes indexam o resultado de uma expressão: CREATE INDEX ON usuarios (LOWER(email)) permite busca case-insensitive sem sequential scan. Index-only scans — quando o índice contém todas as colunas necessárias para a query — evitam acessar a tabela principal, reduzindo I/O drasticamente.
JSONB: banco NoSQL dentro do SQL
JSONB armazena JSON em formato binário decomposicionado — sem reparser a cada leitura, com suporte a índices GIN, e operadores poderosos. Para esquemas semi-estruturados (configurações de usuário, dados de plugins, respostas de APIs externas), JSONB elimina a necessidade de tabelas EAV ou banco NoSQL separado. jsonb_build_object, jsonb_agg e jsonb_extract_path_text permitem construir e consultar JSON complexo diretamente em SQL.
O operador @> (contains) com índice GIN é a feature mais poderosa: WHERE dados @> '{"plano": "premium", "ativo": true}' verifica containment em JSON aninhado e usa o índice eficientemente. Para queries em campos específicos do JSON, crie um índice de expressão: CREATE INDEX ON produtos ((dados->>'categoria')) — permite filtrar por campo JSONB com performance de coluna nativa. jsonb_set e jsonb_insert atualizam campos específicos sem substituir o documento inteiro, essencial para evitar race conditions em updates concorrentes.
CTEs, Window Functions e Aggregations avançadas
Common Table Expressions (WITH) organizam queries complexas em partes legíveis e reutilizáveis dentro da mesma query. Desde o Postgres 12, CTEs não são mais sempre materialized — o otimizador pode “inlinar” a CTE na query principal para usar índices mais eficientemente. Use WITH RECURSIVE para percorrer hierarquias (organograma, threaded comments, bill of materials) sem múltiplas round-trips ao banco — uma query recursiva substitui N queries em código.
Window functions são a feature mais subestimada do SQL moderno. ROW_NUMBER() OVER (PARTITION BY usuario_id ORDER BY criado_em DESC) numera linhas dentro de cada grupo sem GROUP BY. LAG(valor) OVER (ORDER BY data) acessa o valor da linha anterior — perfeito para variações percentuais e detecção de mudanças. SUM(valor) OVER (ORDER BY data ROWS UNBOUNDED PRECEDING) calcula running totals sem self-join. Aggregations como FILTER (WHERE condição) calculam múltiplas agregações condicionais em uma única passagem pela tabela, substituindo múltiplas sub-queries.
Particionamento e manutenção
Tabelas de logs e eventos crescem indefinidamente, mas queries tipicamente acessam apenas dados recentes. Particionamento por range (data) divide a tabela fisicamente em partições menores: CREATE TABLE eventos PARTITION BY RANGE (criado_em). Queries com filtro temporal acessam apenas a partição relevante, reduzindo I/O em ordens de magnitude. Partition pruning é automático — o planner sabe quais partições ignorar. Rotação de partições antigas (DROP TABLE eventos_2023) é instantânea versus DELETE que levaria horas em tabela monolítica.
Manutenção regular evita degradação silenciosa. autovacuum limpa linhas mortas de updates e deletes (bloat de tabela), mas em tabelas de alta escrita pode não acompanhar o ritmo. Monitore bloat com pgstattuple e agende VACUUM FULL em janelas de manutenção para tabelas críticas. REINDEX CONCURRENTLY reconstrói índices sem lock de escrita — essencial para reindexar tabelas de produção. Configurações de work_mem, effective_cache_size e shared_buffers no postgresql.conf têm impacto enorme em performance e merecem tuning baseado no perfil de carga da sua aplicação específica.
Tem um projeto em mente?
Somos especialistas em transformar ideias em produtos digitais. Apps, sites, automações e IA — vamos construir juntos.