Menu

Índice Composto no SQLite: Ordem das Colunas e Prefixo

Como funcionam os índices de múltiplas colunas no SQLite, por que a ordem das colunas faz toda diferença e quando vale a pena criar um índice composto.

Esta página tem editores executáveis — edite, execute e veja a saída na hora.

Um índice, várias colunas

Um índice composto no SQLite — também conhecido como índice de múltiplas colunas — é um único índice construído sobre duas ou mais colunas. Para criar, basta listar as colunas na ordem desejada:

O índice idx_orders_customer_status armazena as entradas ordenadas primeiro por customer_id e, dentro de cada cliente, por status. Essa ordenação é o ponto central — todo o comportamento de um índice composto decorre dela.

O modelo mental: uma lista telefônica ordenada

Imagine uma velha lista telefônica. Os registros estão ordenados por sobrenome e, dentro de cada sobrenome, pelo primeiro nome. É exatamente assim que se comporta um índice em (last_name, first_name).

Algumas buscas saem baratas, outras nem tanto:

  • "Encontrar todo mundo com sobrenome Patel" — tranquilo, todos os Patel ficam juntos.
  • "Encontrar Priya Patel" — tranquilo, vai direto em Patel e percorre até Priya.
  • "Encontrar todo mundo chamado Priya" — lento, você precisa varrer cada página. As Priyas estão espalhadas por todos os sobrenomes.

Um índice composto no SQLite funciona igualzinho. A primeira coluna é a chave principal de ordenação; a segunda coluna só ordena entradas que compartilham o mesmo valor na primeira.

A regra do prefixo mais à esquerda

O SQLite só consegue usar um índice composto numa consulta quando o WHERE restringe um prefixo mais à esquerda das suas colunas. Para um índice em (a, b, c):

  • Filtrando por a — usa o índice.
  • Filtrando por a e b — usa o índice.
  • Filtrando por a, b e c — usa o índice.
  • Filtrando só por b, só por c, ou por b e c — o índice não é usado.

Dá para confirmar isso na prática com EXPLAIN QUERY PLAN:

O primeiro plano mostra SEARCH events USING INDEX idx_events_user_kind_time. Já o segundo cai para SCAN events — filtrar só por kind pula a coluna user_id (que está na frente), então o índice não serve para nada nessa consulta.

A ordem das colunas no índice é uma decisão de projeto

Como o prefixo mais à esquerda é o que manda, a ordem em que você lista as colunas no CREATE INDEX é uma escolha de verdade, não algo estético. Duas regras práticas:

  1. Coloque primeiro a coluna que você mais filtra. É ela que destrava o índice para o maior número de consultas.
  2. Colunas de igualdade vêm antes das de intervalo. O SQLite consegue mergulhar direto no índice usando = e depois varrer uma faixa contígua com <, > ou BETWEEN — mas só na última coluna usada.

O plano mostra SEARCH sales USING INDEX idx_sales_region_time (region=? AND sold_at>?). O SQLite vai direto em region = 'EU' e a partir daí percorre o intervalo de datas. Agora inverta a ordem das colunas para (sold_at, region): a mesma consulta passa a varrer todas as linhas dentro do intervalo de datas e revalidar region em cada uma delas.

Índice composto vs. vários índices de coluna única no SQLite

Uma dúvida bem comum: vale mais criar um índice em (a, b) ou dois índices separados, um em a e outro em b?

Para o filtro combinado, o índice composto é mais rápido — o SQLite vai direto às entradas (project_id, state) que batem com a busca. Com dois índices de coluna única, normalmente o SQLite escolhe um deles, usa-o para reduzir o conjunto de linhas e depois confere a outra coluna em cada linha restante. Em alguns casos ele consegue fazer a interseção dos dois índices, mas, quando as colunas são consultadas juntas, o índice composto resolve de forma muito mais limpa.

Se project_id e state também forem consultados separadamente, talvez valha a pena manter os dois — o composto para o filtro combinado e um índice de coluna única em state para as consultas que filtram só por essa coluna.

Covering index no SQLite

Quando um índice inclui todas as colunas que a consulta precisa — tanto as colunas do filtro quanto as colunas do SELECT — o SQLite consegue responder sem nem encostar na tabela. Isso é um covering index, e é o cenário mais rápido possível para uma consulta.

O plano mostra USING COVERING INDEX idx_invoices_cover. A consulta lê issued_at e total direto do índice — notes e id não são necessários, então a tabela em si nunca é aberta. Acrescentar uma coluna a um índice composto só para cobrir uma query crítica vale a pena quando essa query roda o tempo todo.

Restrições UNIQUE compostas

Índices compostos também garantem unicidade na combinação de colunas. Isso é útil quando nenhuma coluna sozinha é única, mas a combinação delas precisa ser:

O terceiro INSERT dispara UNIQUE constraint failed: enrollments.student_id, enrollments.course_id. O par já existe no índice, então o SQLite recusa a duplicata.

Armadilhas que vale a pena conhecer

  • OR entre colunas que não são a primeira derruba o índice. Um WHERE a = 1 OR b = 2 sobre um índice (a, b) normalmente não consegue usar o índice — o SQLite precisa avaliar cada ramo separadamente.
  • Funções em colunas indexadas desativam o índice. WHERE lower(email) = 'x' não usa um índice em email. A saída é indexar a expressão ou normalizar o dado já na inserção.
  • Índice tem custo. Todo índice é atualizado a cada INSERT, UPDATE (das colunas indexadas) e DELETE. Três índices compostos numa tabela com muita escrita podem dominar o custo das gravações.
  • Rode ANALYZE depois de criar índices. O planejador do SQLite usa as estatísticas coletadas pelo ANALYZE para escolher entre índices candidatos. Sem essas estatísticas, ele recorre a heurísticas que nem sempre acertam.

Um fluxo de trabalho prático

Quando você está afinando uma consulta lenta, o ciclo costuma ser este:

  1. Rode EXPLAIN QUERY PLAN na consulta para ver o que o SQLite está fazendo hoje.
  2. Se estiver fazendo scan, olhe a cláusula WHERE — qual é a coluna de igualdade? Qual é a de intervalo? Quais colunas estão no SELECT?
  3. Monte um índice composto com a coluna de igualdade primeiro, a de intervalo depois, e acrescente as colunas selecionadas se um covering index ajudar.
  4. Rode ANALYZE.
  5. Rode EXPLAIN QUERY PLAN de novo. Confirme que o plano mudou e que o índice está sendo usado.
  6. Meça o tempo da consulta antes e depois, com dados representativos.

Pular o passo 6 é por sua conta e risco. Um índice que parece certo no plano ainda pode ser mais lento na prática se a tabela for pequena ou se o planejador escolher outro caminho.

A seguir: índices parciais

Os índices compostos cobrem todas as linhas da tabela. Mas muitas vezes só um subconjunto pequeno de linhas importa — tickets abertos, jobs ainda não processados, registros não deletados. Um índice parcial permite indexar apenas essas linhas, com uma cláusula WHERE embutida no próprio índice. É o tema da próxima página.

Perguntas frequentes

O que é um índice composto no SQLite?

É um único índice que cobre duas ou mais colunas. Você cria com CREATE INDEX idx_name ON tabela(col_a, col_b). O SQLite guarda as entradas ordenadas primeiro por col_a e, dentro de cada valor de col_a, por col_b — funciona igual a uma agenda telefônica ordenada por sobrenome e, depois, por nome.

A ordem das colunas faz diferença num índice composto?

Faz, e muita. O SQLite só consegue usar um índice composto se o WHERE filtrar por um prefixo mais à esquerda das colunas indexadas. Um índice em (a, b, c) ajuda consultas que filtram por a, por a e b, ou pelas três juntas — mas não serve para nada se você filtrar só por b ou só por c.

Quando usar um índice composto em vez de vários índices de uma coluna só?

Use índice composto quando suas consultas costumam filtrar ou ordenar pela mesma combinação de colunas. Já índices separados de coluna única funcionam melhor quando cada coluna é consultada de forma independente. Rode EXPLAIN QUERY PLAN para ver qual índice o SQLite realmente escolhe — esse é o único feedback confiável.

O que é um covering index no SQLite?

Um covering index inclui todas as colunas que a consulta precisa, então o SQLite responde direto pelo índice, sem nem encostar na tabela. O EXPLAIN QUERY PLAN mostra USING COVERING INDEX quando isso acontece. Adicionar colunas extras a um índice composto só para cobrir uma consulta crítica é uma otimização clássica.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR