PostgreSQL: Índices que Fazem Diferença na Performance
Por que Índices Importam?
Sem índice, o PostgreSQL faz sequential scan — lê todas as linhas da tabela. Com 1 milhão de registros, uma query de 100ms vira 10 segundos. O índice certo transforma isso de volta em milissegundos.
EXPLAIN ANALYZE: Seu Melhor Amigo
Antes de criar qualquer índice, use EXPLAIN ANALYZE para entender o plano de execução:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM pedidos
WHERE usuario_id = 123
AND status = 'pendente'
AND criado_em > NOW() - INTERVAL '30 days';Procure por:
Seq Scanem tabelas grandes → candidato a índicecost=X..Yalto → query caraactual rowsmuito diferente derowsestimado → estatísticas desatualizadas (ANALYZE tabela)
Tipos de Índice
B-tree (Padrão)
Para igualdade, ranges e ordenação:
-- Índice simples
CREATE INDEX idx_pedidos_usuario ON pedidos(usuario_id);
-- Índice composto — ordem importa!
-- Útil para: WHERE usuario_id = X AND status = Y
-- Útil para: WHERE usuario_id = X (apenas primeiro campo)
-- NÃO útil para: WHERE status = Y (apenas segundo campo)
CREATE INDEX idx_pedidos_usuario_status ON pedidos(usuario_id, status);
-- Índice com ordenação para ORDER BY + LIMIT
CREATE INDEX idx_pedidos_criado_desc ON pedidos(criado_em DESC);Índice Parcial
Indexa apenas um subconjunto das linhas — menor e mais rápido:
-- Apenas pedidos pendentes (talvez 5% do total)
CREATE INDEX idx_pedidos_pendentes
ON pedidos(usuario_id, criado_em)
WHERE status = 'pendente';
-- Apenas usuários ativos
CREATE INDEX idx_usuarios_email_ativos
ON usuarios(email)
WHERE ativo = true;GIN — Full Text Search e Arrays
-- Full text search em português
ALTER TABLE artigos ADD COLUMN ts_busca tsvector
GENERATED ALWAYS AS (
to_tsvector('portuguese', coalesce(titulo, '') || ' ' || coalesce(conteudo, ''))
) STORED;
CREATE INDEX idx_artigos_fts ON artigos USING GIN(ts_busca);
-- Busca
SELECT titulo FROM artigos
WHERE ts_busca @@ plainto_tsquery('portuguese', 'postgresql performance');
-- Arrays
CREATE INDEX idx_tags ON posts USING GIN(tags);
SELECT * FROM posts WHERE tags @> ARRAY['postgresql', 'performance'];BRIN — Tabelas Muito Grandes com Dados Sequenciais
-- Para tabelas de logs onde criado_em é quase sequencial
-- Índice minúsculo, eficiente para ranges de datas
CREATE INDEX idx_logs_criado_brin ON logs USING BRIN(criado_em);Cobrindo Queries com Include
-- Index-only scan: evita acessar a tabela completamente
CREATE INDEX idx_pedidos_cobrindo
ON pedidos(usuario_id, status)
INCLUDE (valor_total, criado_em);
-- Esta query nunca toca a tabela principal
SELECT status, valor_total, criado_em
FROM pedidos
WHERE usuario_id = 123;Identificando Índices Não Usados
-- Índices que nunca foram usados desde o último reset
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;Índices não usados têm custo: degradam writes e ocupam espaço. Remova os desnecessários.
Índices Concorrentes (Produção)
-- Cria o índice sem bloquear writes — demora mais, mas é seguro em produção
CREATE INDEX CONCURRENTLY idx_pedidos_novo ON pedidos(campo);
-- Remover também pode ser concorrente
DROP INDEX CONCURRENTLY idx_pedidos_antigo;pg_stat_statements: Encontrando Queries Lentas
-- Habilite no postgresql.conf:
-- shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Top 10 queries mais lentas
SELECT
LEFT(query, 80) AS query,
calls,
ROUND(mean_exec_time::numeric, 2) AS media_ms,
ROUND(total_exec_time::numeric / 1000, 2) AS total_seg
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;Manutenção
-- Atualiza estatísticas usadas pelo planner
ANALYZE tabela;
-- Reconstrói índices fragmentados (cria novo, troca, remove antigo)
REINDEX INDEX CONCURRENTLY idx_pedidos_usuario;
-- Verifica inchaço de índices
SELECT indexrelid::regclass AS indice,
pg_size_pretty(pg_relation_size(indexrelid)) AS tamanho
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 20;Conclusão
Índices são a ferramenta mais poderosa para melhorar performance de banco de dados. Comece sempre com EXPLAIN ANALYZE, crie índices parciais quando possível, e monitore regularmente índices não usados. Um índice criado errado pode piorar a situação — entenda o plano de execução antes de agir.