Índices que funcionam: B-tree, hash, GIN, covering, composto
Um índice mal criado pode ser tão ruim quanto nenhum índice. Entender quando o planner usa um índice, quando não usa (e por quê), e como criar índices que realmente eliminam o trabalho do banco — essa é a diferença entre queries de 100ms e 10s.
Como funciona um índice B-tree
Um índice B-tree é uma árvore balanceada onde as folhas contêm os valores indexados e ponteiros para as linhas na tabela (heap). Buscar por um valor é O(log n) — varrer a tabela seria O(n). O banco mantém o índice atualizado em toda inserção, atualização e deleção.
-- Criando índices básicos
CREATE INDEX idx_pedidos_cliente ON pedidos(cliente_id);
CREATE INDEX idx_pedidos_status ON pedidos(status);
CREATE INDEX idx_pedidos_criado ON pedidos(criado_em);
-- Verificar índices de uma tabela:
d pedidos -- no psql
-- ou:
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'pedidos';
-- Quando o índice É usado:
-- ✅ WHERE cliente_id = 42 -- equality
-- ✅ WHERE cliente_id IN (1, 2, 3) -- IN list
-- ✅ WHERE criado_em > '2024-01-01' -- range scan
-- ✅ ORDER BY cliente_id LIMIT 10 -- sort com limit (index scan evita sort)
-- ✅ COUNT(*) com WHERE indexado -- pode usar index scan
-- Quando o índice NÃO é usado (ou não deveria ser):
-- ❌ WHERE UPPER(email) = 'USER@EX.COM' -- função na coluna destrói índice
-- ❌ WHERE id + 1 = 42 -- expressão na coluna indexada
-- ❌ WHERE status LIKE '%pendente%' -- % no início impede uso de índice
-- ❌ SELECT * FROM pedidos -- sem WHERE, Seq Scan é melhor
-- ❌ Tabelas muito pequenas (Seq Scan é mais rápido)
-- Para funções na coluna — índice funcional/expressão:
CREATE INDEX idx_email_lower ON clientes(LOWER(email));
-- Agora funciona: WHERE LOWER(email) = 'user@ex.com'Índices compostos: a ordem importa
-- Índice composto — leftmost prefix rule
CREATE INDEX idx_pedidos_cliente_status ON pedidos(cliente_id, status);
-- Usa o índice ✅ (prefixo da esquerda):
SELECT * FROM pedidos WHERE cliente_id = 42;
SELECT * FROM pedidos WHERE cliente_id = 42 AND status = 'pendente';
-- NÃO usa o índice ❌ (só coluna da direita):
SELECT * FROM pedidos WHERE status = 'pendente';
-- Solução: criar índice separado em (status)
-- Ordem das colunas no índice composto importa:
-- (cliente_id, status): bom para queries por cliente, ou (cliente, status)
-- (status, cliente_id): bom para queries por status, ou (status, cliente)
-- Regra: coluna mais seletiva (menos valores repetidos) primeiro — geralmente
-- Índice composto para ORDER BY eficiente:
CREATE INDEX idx_criado_status ON pedidos(criado_em DESC, status);
-- Agora: SELECT * FROM pedidos ORDER BY criado_em DESC LIMIT 10
-- usa Index Scan sem Sort adicional!- → não alcança
- → não alcança
- Conceito de arquitetura
A pergunta que substitui a intuição: quais são as combinações de filtro que o código realmente executa? A ordem dos campos sai daí — e um índice composto bem ordenado costuma substituir três índices separados.
- 1 · O índice é uma lista ordenada, e a ordem é hierárquica. Primeiro por estado; dentro de cada estado, por cidade; dentro de cada cidade, por data. Como uma lista telefônica por sobrenome e depois nome.
- 2 · Só se entra pela esquerda. Procurar por cidade sem saber o estado é procurar um nome numa lista ordenada por sobrenome: não há atalho, e o banco lê tudo.
- 3 · Prefixo basta — não precisa usar tudo. Filtrar só por estado aproveita o índice normalmente. É por isso que a ordem dos campos importa mais que a quantidade.
- 4 · A ordem certa vem das consultas, não da intuição. O campo mais seletivo primeiro é regra de bolso e pode enganar: o que decide é quais combinações de filtro aparecem de verdade no seu código.
- 5 · Índice tem custo de escrita. Cada um precisa ser atualizado em toda inserção, atualização e remoção. Cinco índices numa tabela de escrita intensa é uma decisão, não um detalhe.
- 6 · Índice não usado é só custo. Ele ocupa disco, deixa a escrita mais lenta e não acelera nada. Medir o uso e remover os inúteis costuma render mais que criar novos.
Numa consulta que filtra por `estado` e `cidade`, existe um índice composto `(cidade, estado)`. Ele é usado com eficiência para uma consulta que filtra apenas por `estado`?
Tipos de índice e casos especiais
| Tipo | Operadores suportados | Melhor para | Cuidado |
|---|---|---|---|
| B-tree (padrão) | =, <, >, <=, >=, BETWEEN, LIKE prefix | Tudo que é comparável | Não serve para @> @? no JSONB |
| Hash | = apenas | Equality pura, mais rápido que B-tree | Não suporta range, ORDER BY |
| GIN | @>, @?, @@, && | Arrays, JSONB, full-text (tsvector) | Update lento, maior em memória |
| GiST | &&, @>, @<, <<, |>> | Geospatial, ranges, exclusão | Mais complexo de criar |
| BRIN | =, <, >, BETWEEN | Tabelas enormes ordenadas (logs, IoT) | Muito impreciso, lê mais blocos |
-- Índice parcial — indexa só parte da tabela
CREATE INDEX idx_pedidos_pendentes ON pedidos(criado_em)
WHERE status = 'pendente';
-- Índice menor, mais rápido, menos memória
-- Só funciona para queries que incluem o WHERE do índice
-- Índice GIN para JSONB e arrays
CREATE INDEX idx_produtos_jsonb ON produtos USING GIN (atributos);
-- Habilita: WHERE atributos @> '{"cor": "azul"}'
-- e: WHERE atributos ? 'desconto' (chave existe)
CREATE INDEX idx_artigos_tags ON artigos USING GIN (tags);
-- Habilita: WHERE tags @> ARRAY['python', 'sql']
-- Índice GIN para full-text search
CREATE INDEX idx_artigos_fts ON artigos
USING GIN (to_tsvector('portuguese', titulo || ' ' || corpo));
-- Habilita: WHERE to_tsvector('portuguese', titulo || ' ' || corpo)
-- @@ to_tsquery('portuguese', 'sql & performance')
-- Covering index com INCLUDE
CREATE INDEX idx_pedidos_covering ON pedidos(cliente_id)
INCLUDE (total, status, criado_em);
-- Query: SELECT total, status FROM pedidos WHERE cliente_id = 42
-- Resultado: Index-Only Scan — não toca no heap
-- Criar índice sem bloquear produção (CONCURRENTLY)
CREATE INDEX CONCURRENTLY idx_novo ON tabela_grande(coluna);
-- Demora mais, mas não bloqueia writes durante a criaçãoRegras práticas: índice em foreign keys é obrigatório (PostgreSQL não cria automaticamente, ao contrário do MySQL). Índice em colunas frequentes no WHERE e ORDER BY. Use INCLUDE para covering indexes em queries de leitura intensa. Crie CONCURRENTLY em produção. Monitore índices não usados: pg_stat_user_indexes mostra idx_scan = 0.
Próximo: EXPLAIN ANALYZE — como ler o plano de execução e transformar queries lentas em rápidas.
Perguntas frequentes
❓ Por que meu índice não é usado?
❓ Quando índice composto ajuda mais que dois índices?
❓ Índice de cobertura vale a pena?
Fixando
Por que criar índices demais é prejudicial?
Uma consulta com filtro por igualdade não usa o índice existente naquela coluna. Qual é a causa mais comum?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…