Indexação

Í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.