Índices compostos no MySQL: a regra do prefixo mais à esquerda
Entenda como índices de múltiplas colunas funcionam no MySQL, por que a ordem das colunas define tudo e como acertar de primeira.
Índice composto é a otimização com melhor custo-benefício do MySQL — e a mais mal entendida. O erro clássico: criar índices separados para cada coluna do WHERE e não entender por que o MySQL continua fazendo varredura completa. A resposta está em uma regra: o prefixo mais à esquerda.
Como um índice composto é armazenado
Um índice de múltiplas colunas é uma lista ordenada pela concatenação das colunas — como uma lista telefônica ordenada por sobrenome, depois nome. O índice:
CREATE INDEX idx_pedidos ON pedidos (status, cliente_id, criado_em);
guarda as entradas ordenadas por status, e dentro de cada status por cliente_id, e dentro de cada cliente por criado_em. É como se você tivesse criado, na prática, três índices:
(status)(status, cliente_id)(status, cliente_id, criado_em)
E nenhum outro. O índice não atende (cliente_id) sozinho nem (cliente_id, criado_em) sem o status.
A regra em ação
Com o índice (status, cliente_id, criado_em):
-- Usa o índice: prefixo completo (status)
SELECT * FROM pedidos WHERE status = 'pago';
-- Usa o índice: prefixo (status, cliente_id)
SELECT * FROM pedidos
WHERE status = 'pago' AND cliente_id = 4821;
-- NÃO usa eficientemente: quebra o prefixo na 1ª coluna
SELECT * FROM pedidos
WHERE cliente_id = 4821 AND criado_em >= '2026-01-01';
O terceiro caso é o que surpreende: mesmo com cliente_id e criado_em no índice, a ausência do status no filtro deixa o MySQL sem o ponto de partida ordenado. Ele pode fazer um index skip scan (MySQL 8.0.13+) quando a cardinalidade da primeira coluna é baixa, mas é exceção, não regra.
Colunas de intervalo vão por último
Quando o filtro mistura igualdade e intervalo, a igualdade vem primeiro no índice:
-- Consulta:
SELECT * FROM pedidos
WHERE status = 'pago'
AND criado_em >= '2026-08-01';
Índice (status, criado_em) — perfeito: posiciona direto nos pedidos pagos de agosto. Índice (criado_em, status) — ruim: entra pelo intervalo de datas e ainda filtra status lendo cada entrada. Depois de uma coluna de intervalo, nenhuma coluna seguinte do índice é usada para busca (apenas para ICP e covering).
A armadilha dos índices redundantes
CREATE INDEX idx1 ON pedidos (status);
CREATE INDEX idx2 ON pedidos (status, cliente_id);
O idx1 é 100% redundante — todo prefixo já existe em idx2. Índice redundante só adiciona custo de escrita em cada INSERT/UPDATE/DELETE sem nunca ser escolhido. Remova-o:
ALTER TABLE pedidos DROP INDEX idx1;
Como validar
Confirme o uso real com o plano de execução:
EXPLAIN SELECT * FROM pedidos
WHERE status = 'pago' AND cliente_id = 4821;
Procure key = idx_pedidos e type = ref. Se aparecer type = ALL ou um key inesperado, o prefixo foi quebrado. O EXPLAIN ANALYZE confirma com tempo real de execução.
Conclusão
Pense no índice composto como lista telefônica: só ajuda se você buscar pela ordem em que ela está ordenada. Igualdades primeiro, intervalo por último, e nada de colunas sobrando à esquerda. Para um desenho de índices completo do seu schema — incluindo análise de redundâncias e covering indexes — veja os serviços de consultoria da MySQL Box.