Otimizando queries com índices compostos
1. Fundamentos dos Índices Compostos
Um índice composto é uma estrutura de dados que armazena múltiplas colunas de uma tabela em uma única entrada de índice, organizada em uma árvore B-Tree. Diferentemente de um índice simples, que contém apenas uma coluna, o índice composto permite que o banco de dados localize registros com base em combinações de valores de forma muito mais eficiente.
A principal vantagem do índice composto está na cardinalidade combinada das colunas. Enquanto um índice simples em uma coluna de baixa seletividade (como status com apenas 3 valores distintos) ainda exigirá que o banco examine muitas linhas, um índice composto com colunas de alta seletividade (como status + data_criacao + cliente_id) pode reduzir drasticamente o número de linhas examinadas.
A ordem das colunas no índice é o fator mais crítico. A regra fundamental é: coloque as colunas mais seletivas primeiro. Isso maximiza a capacidade do índice de filtrar registros logo nos primeiros níveis da árvore B-Tree.
2. O Princípio do Prefixo Mais à Esquerda (Leftmost Prefix)
O princípio do prefixo mais à esquerda determina que um índice composto só pode ser utilizado quando a query referencia as colunas mais à esquerda do índice, em ordem. Considere um índice composto definido como:
CREATE INDEX idx_pedidos_status_data
ON pedidos (status, data_criacao, cliente_id);
Esta query aproveita o índice completamente:
SELECT * FROM pedidos
WHERE status = 'PENDENTE'
AND data_criacao >= '2024-01-01'
AND cliente_id = 123;
Esta query aproveita apenas as duas primeiras colunas:
SELECT * FROM pedidos
WHERE status = 'PENDENTE'
AND data_criacao >= '2024-01-01';
Esta query NÃO utiliza o índice:
SELECT * FROM pedidos
WHERE data_criacao >= '2024-01-01'
AND cliente_id = 123;
A razão é que a coluna data_criacao não é a primeira coluna do índice, e não há referência à coluna status para iniciar a busca.
3. Estratégias de Ordenação de Colunas
Para maximizar a eficiência, siga esta ordem recomendada:
- Colunas de igualdade primeiro (WHERE coluna = valor)
- Colunas de ordenação (ORDER BY)
- Colunas de range por último (BETWEEN, >, <)
Exemplo prático:
-- Query frequente
SELECT * FROM vendas
WHERE loja_id = 10
AND data_venda BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY valor_total DESC;
-- Índice otimizado
CREATE INDEX idx_vendas_loja_data_valor
ON vendas (loja_id, data_venda, valor_total DESC);
Neste caso, loja_id é filtro de igualdade, data_venda é range, e valor_total é usado para ordenação. O índice cobre todos os três aspectos.
4. Índices Compostos e Cobertura de Queries (Covering Index)
Um índice de cobertura contém todas as colunas necessárias para responder uma query, eliminando a necessidade de acessar a tabela principal. Isso é chamado de Index-Only Scan.
Exemplo:
-- Tabela
CREATE TABLE produtos (
id INT PRIMARY KEY,
nome VARCHAR(100),
categoria VARCHAR(50),
preco DECIMAL(10,2),
estoque INT
);
-- Índice composto de cobertura
CREATE INDEX idx_produtos_categoria_preco_estoque
ON produtos (categoria, preco, estoque);
-- Query que usa apenas o índice
SELECT categoria, preco, estoque
FROM produtos
WHERE categoria = 'ELETRÔNICOS'
AND preco BETWEEN 100 AND 500;
O banco de dados pode responder esta query inteiramente a partir do índice, sem acessar a tabela produtos. O ganho de performance é significativo, mas o custo é o aumento do tamanho do índice e maior lentidão em operações de escrita.
5. Análise Prática com EXPLAIN
Vamos comparar o plano de execução antes e depois de criar um índice composto:
-- Antes do índice composto
EXPLAIN ANALYZE
SELECT * FROM transacoes
WHERE conta_id = 42
AND data BETWEEN '2024-01-01' AND '2024-03-31'
AND tipo = 'DEBITO';
-- Saída típica (sem índice):
-- Seq Scan on transacoes (cost=0.00..1843.00 rows=150 width=120)
-- Filter: ((conta_id = 42) AND (data >= '2024-01-01') AND (tipo = 'DEBITO'))
-- Após criar o índice
CREATE INDEX idx_transacoes_conta_data_tipo
ON transacoes (conta_id, data, tipo);
-- Novo EXPLAIN ANALYZE:
-- Index Scan using idx_transacoes_conta_data_tipo on transacoes
-- (cost=0.29..8.30 rows=150 width=120)
-- Index Cond: ((conta_id = 42) AND (data >= '2024-01-01') AND (tipo = 'DEBITO'))
Observe a mudança de Seq Scan (varredura sequencial) para Index Scan. O custo cai drasticamente de 1843 para 8.30.
6. Casos de Uso Comuns e Anti-Padrões
Caso real de sucesso:
-- Sistema de e-commerce: buscar pedidos recentes de um cliente
CREATE INDEX idx_pedidos_cliente_data_status
ON pedidos (cliente_id, data_pedido DESC, status);
-- Query otimizada
SELECT id, data_pedido, status, valor_total
FROM pedidos
WHERE cliente_id = 789
AND data_pedido >= CURRENT_DATE - INTERVAL '30 days'
AND status IN ('PENDENTE', 'PROCESSANDO');
Anti-padrão: índice com coluna de baixa seletividade no início.
-- ERRO: coluna 'ativo' tem apenas 2 valores (0 ou 1)
CREATE INDEX idx_usuarios_ativo_nome
ON usuarios (ativo, nome);
-- Query que sofre com isso
SELECT * FROM usuarios
WHERE ativo = 1 AND nome = 'João Silva';
-- O índice examinará 50% da tabela antes de encontrar 'João Silva'
Quando evitar: Em tabelas com menos de 1000 registros, o custo de manter o índice geralmente supera o benefício.
7. Manutenção e Monitoramento de Índices Compostos
Índices compostos impactam operações de escrita. Cada INSERT, UPDATE ou DELETE precisa atualizar todas as entradas do índice. Em tabelas com alta frequência de escrita, índices demais podem degradar a performance.
Para identificar índices não utilizados:
-- PostgreSQL: consultar estatísticas de uso
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
Índices com idx_scan próximo de zero são candidatos a remoção.
Para reconstruir índices fragmentados:
-- PostgreSQL
REINDEX INDEX idx_pedidos_cliente_data_status;
-- SQL Server
ALTER INDEX idx_pedidos_cliente_data_status ON pedidos REBUILD;
8. Considerações Avançadas
Índices compostos com expressões:
-- Índice funcional composto
CREATE INDEX idx_clientes_nome_lower_data
ON clientes (LOWER(nome), data_cadastro);
-- Query que o utiliza
SELECT * FROM clientes
WHERE LOWER(nome) = 'maria silva'
AND data_cadastro > '2024-01-01';
Índices compostos em joins:
-- Otimizando join entre tabelas grandes
CREATE INDEX idx_pedidos_cliente_foreign
ON pedidos (cliente_id, data_pedido);
SELECT c.nome, p.id, p.data_pedido
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id
WHERE c.uf = 'SP'
ORDER BY p.data_pedido DESC;
Combinação com índices parciais:
-- Índice composto parcial para cenário específico
CREATE INDEX idx_pedidos_atrasados_cliente
ON pedidos (cliente_id, data_vencimento)
WHERE status = 'PENDENTE' AND data_vencimento < CURRENT_DATE;
Este índice só armazena pedidos pendentes e vencidos, sendo muito menor e mais rápido que um índice completo.
Dominar índices compostos é essencial para qualquer profissional que trabalhe com bancos de dados relacionais. A chave está em entender o padrão de consultas da aplicação e projetar índices que cubram esses padrões de forma eficiente, sempre equilibrando ganhos de leitura com custos de escrita.
Referências
-
PostgreSQL Documentation: Indexes — Documentação oficial sobre criação e uso de índices no PostgreSQL, incluindo índices compostos e dicas de performance.
-
Use the Index, Luke! - A Guide to Database Performance — Guia completo e prático sobre otimização de consultas com índices, com exemplos em vários bancos de dados.
-
MySQL 8.0 Reference Manual: Multiple-Column Indexes — Documentação oficial do MySQL sobre índices de múltiplas colunas e o princípio do prefixo mais à esquerda.
-
SQL Server Index Design Guide — Guia oficial da Microsoft para design de índices no SQL Server, incluindo estratégias para índices compostos.
-
Oracle Database: Managing Indexes — Documentação Oracle sobre gerenciamento de índices, com foco em índices compostos e cobertura de consultas.
-
Brent Ozar: How to Think Like the SQL Server Engine — Artigo técnico que explica como o otimizador de consultas decide usar índices, incluindo exemplos com índices compostos.