icardb
PostgreSQL além do básico: índices, EXPLAIN e JSONB na prática
Voltar para artigosPROGRAMAÇÃO

PostgreSQL além do básico: índices, EXPLAIN e JSONB na prática

Por Lucas Mendes 7 min de leitura

Saber SELECT, JOIN e GROUP BY coloca qualquer desenvolvedor em operação. O que separa o uso básico do avançado de PostgreSQL é entender como o banco executa suas queries — e como convencê-lo a executá-las melhor. Este artigo cobre os três temas que mais impactam performance em produção: leitura de planos com EXPLAIN ANALYZE, escolha do tipo de índice correto e uso responsável de JSONB.

EXPLAIN ANALYZE: lendo o que o banco realmente fez

EXPLAIN mostra o plano que o otimizador escolheu; EXPLAIN ANALYZE executa a query de verdade e compara estimativa com realidade. Os três nós de scan que você precisa reconhecer:

Nó no planoO que significaQuando apareceÉ ruim?
Seq ScanLê a tabela inteira, linha a linhaSem índice útil ou tabela pequenaEm tabela grande com filtro seletivo, sim
Index ScanUsa índice para localizar, depois lê a linha no heapFiltro seletivo com índice disponívelNão; é o caminho normal
Index Only ScanResolve tudo dentro do índice, sem tocar o heapColunas da query cobertas pelo índiceNão; é o mais rápido
Bitmap Heap ScanMarca páginas via índice e lê em blocoFiltro pouco seletivo (muitas linhas)Não; é mais barato que N Index Scans
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, email FROM usuarios WHERE email = 'ana@exemplo.com';

-- Antes do índice:
-- Seq Scan on usuarios  (cost=0.00..18334.00 rows=1)
--   Filter: (email = 'ana@exemplo.com')
--   Execution Time: 142.318 ms

CREATE INDEX idx_usuarios_email ON usuarios (email);

-- Depois do índice:
-- Index Scan using idx_usuarios_email on usuarios  (cost=0.43..8.45 rows=1)
--   Index Cond: (email = 'ana@exemplo.com')
--   Execution Time: 0.061 ms

Seq Scan não é sempre problema. Em tabelas pequenas (algumas centenas de páginas) ou quando a query retorna mais de ~10-20% das linhas, ler a tabela inteira em sequência é genuinamente mais rápido que pular entre índice e heap.

Tipos de índice: B-tree não é o único

O B-tree é o índice padrão e atende igualdade e ranges em dados ordenáveis. Mas PostgreSQL oferece estruturas especializadas que mudam ordens de magnitude em casos específicos:

TipoIdeal paraExemplo de uso
B-treeIgualdade, <, >, ORDER BY em colunas ordenáveisidx em email, created_at
GINValores compostos: arrays, JSONB, full-text searchtags @> ARRAY['sql'], jsonb @> '{"ativo":true}'
GiSTDados geométricos e ranges com sobreposiçãoPostGIS, tstzrange
BRINTabelas enormes com correlação física naturallogs ordenados por tempo (índice minúsculo)
HashIgualdade simples (raramente melhor que B-tree)casos muito específicos
sql
-- Índice GIN para busca dentro de coluna JSONB
CREATE INDEX idx_eventos_payload ON eventos USING GIN (payload);

-- Agora esta query usa o índice:
SELECT * FROM eventos WHERE payload @> '{"tipo": "pagamento"}';

-- Índice parcial: só indexa o que interessa (menor e mais rápido)
CREATE INDEX idx_pedidos_abertos ON pedidos (created_at)
WHERE status = 'aberto';

-- Índice por expressão: acelera busca case-insensitive
CREATE INDEX idx_usuarios_email_lower ON usuarios (lower(email));
SELECT * FROM usuarios WHERE lower(email) = 'ana@exemplo.com';

JSONB sem destruir a performance

JSONB é legítimo para dados genuinamente semiestruturados: payloads de webhook, preferências de usuário, metadados variáveis por tenant. O problema é o anti-padrão de usar JSONB para escapar de modelagem — campos que toda linha tem e que aparecem em WHEREs frequentes merecem colunas de verdade, com tipo, NOT NULL e índice B-tree.

  • Use JSONB (não JSON): o formato binário indexável e sem duplicatas de chave.
  • Busca por chaves arbitrárias: índice GIN na coluna inteira.
  • Busca sempre pelo mesmo caminho: índice B-tree por expressão, ex. (payload->>'tipo').
  • Validação de estrutura: aplique CHECK com jsonb_typeof ou valide na aplicação; o banco não impõe schema dentro do JSONB.
sql
-- Índice de expressão para o caminho mais consultado
CREATE INDEX idx_eventos_tipo ON eventos ((payload->>'tipo'));

-- Extração de campos: -> retorna jsonb, ->> retorna text
SELECT payload->>'tipo'   AS tipo,
       payload->'dados'->>'valor' AS valor
FROM eventos
WHERE payload->>'tipo' = 'pagamento';

Manutenção: VACUUM, estatísticas e índices mortos

PostgreSQL usa MVCC: updates criam novas versões das linhas e as antigas ficam como 'dead tuples' até o autovacuum limpar. Em tabelas com muito update, monitore bloat e ajuste autovacuum_vacuum_scale_factor. Estatísticas desatualizadas fazem o otimizador escolher planos ruins — ANALYZE na tabela resolve. E índices que nenhuma query usa continuam custando escrita: consulte pg_stat_user_indexes para encontrá-los.

sql
-- Índices que nunca foram usados desde o último reset de estatísticas
SELECT schemaname, relname, indexrelname, idx_scan,
       pg_size_pretty(pg_relation_size(indexrelid)) AS tamanho
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;

Conclusão

Performance em PostgreSQL é um loop de três passos: medir com EXPLAIN ANALYZE, escolher a estrutura de índice adequada ao padrão de acesso e manter estatísticas e vacuum saudáveis. JSONB é uma ferramenta poderosa quando usada para dados realmente variáveis — não como substituto de modelagem. Domine esses três pilares e a grande maioria dos problemas de lentidão deixa de ser mistério.

Perguntas frequentes

+Por que meu índice existe mas o PostgreSQL não usa?

Causas comuns: estatísticas desatualizadas (rode ANALYZE), o filtro retorna uma fração grande da tabela (Seq Scan é mais barato), função aplicada sobre a coluna sem índice de expressão correspondente (ex.: lower(email) sem índice em lower(email)), ou tipo do parâmetro diferente do tipo da coluna.

+JSONB ou colunas normais: como decidir?

Pergunte: esse campo aparece em WHERE, JOIN ou ORDER BY com frequência? Toda linha o possui? Se sim para ambos, merece coluna tipada. JSONB serve para atributos esparsos e variáveis entre linhas.

+Quantos índices são demais?

Cada índice torna INSERT/UPDATE/DELETE mais caros e ocupa disco. Não há número mágico; o critério é uso real. Audite periodicamente com pg_stat_user_indexes e remova o que não serve consulta nenhuma.

+Qual a diferença entre EXPLAIN e EXPLAIN ANALYZE?

EXPLAIN mostra o plano estimado sem executar. EXPLAIN ANALYZE executa de verdade e mostra tempos e contagens reais lado a lado com as estimativas — discrepâncias grandes indicam estatísticas ruins. Cuidado: ele executa a query; evite em escritas em produção.

Fontes consultadas

Revisão editorial: publicado em . Última revisão em . Conteúdo educativo, sem patrocínio das ferramentas citadas.

Crédito da imagem: Foto: Lucas Mendes / Gerado por IA (Licença Editorial)

Leia também