Um indice composto cobre varias colunas em uma unica estrutura. A ordem certa das colunas transforma filtro, ordenacao e ate a leitura das colunas necessarias em uma unica passagem pelo indice; a ordem errada o deixa quase inutil.
A regra do prefixo a esquerda
Uma B-tree ordena as linhas pelas colunas da esquerda para a direita, como palavras num dicionario. Por isso o indice so e aproveitado a partir da coluna mais a esquerda e sem lacunas.
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);
WHERE user_id = 42 funciona, e o prefixo a esquerda.
WHERE user_id = 42 AND created_at > now() - interval '7 days' usa as duas colunas.
WHERE created_at > now() - interval '7 days' nao consegue usar este indice: a segunda coluna e inalcancavel sem a primeira.
Gotcha: a igualdade na primeira coluna "destrava" a ordenacao e os intervalos na segunda. Mas um intervalo na primeira coluna mata a busca pontual na segunda, porque depois de > ou < as linhas ja nao estao ordenadas pela coluna seguinte.
Ordem das colunas: filtro, depois ordenacao, depois intervalo
Regra pratica: primeiro as colunas de igualdade, depois a coluna de ordenacao e por ultimo a coluna de intervalo.
SELECT id, amount, status
FROM orders
WHERE user_id = 42
AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;
O indice (user_id, created_at DESC) e ideal: user_id restringe por igualdade e created_at DESC devolve as linhas ja ordenadas, entao nao e preciso um passo de ordenacao a parte.
- A igualdade (
=, IN) vem primeiro.
- A coluna do
ORDER BY vem logo depois das igualdades.
- O intervalo (
>, <, BETWEEN) vem por ultimo, porque quebra a ordem de tudo o que vem depois no indice.
Cobrir um ORDER BY e a direcao da ordenacao
Para o indice eliminar o passo de ordenacao, a direcao do ORDER BY precisa coincidir com o indice ou ser o seu inverso completo. O PostgreSQL consegue percorrer uma B-tree nos dois sentidos.
CREATE INDEX idx_emp_dept_salary
ON employees (dept, salary DESC);
SELECT name, salary
FROM employees
WHERE dept = 'eng'
ORDER BY salary DESC
LIMIT 10;
ORDER BY salary ASC tambem funciona sem ordenar, e uma varredura reversa. Mas um ORDER BY dept ASC, salary ASC misto contra um indice (dept, salary DESC) forca uma reordenacao: as direcoes por coluna nao podem ser misturadas a vontade.
Colunas INCLUDE e index-only scan
Se o indice contem todas as colunas que a consulta precisa, o motor nao toca na tabela, e um index-only scan. As colunas pelas quais voce nao filtra ficam melhor adicionadas via INCLUDE: elas vivem so nas folhas e nao incham a navegacao pela arvore.
CREATE INDEX idx_orders_user_inc
ON orders (user_id, created_at DESC)
INCLUDE (amount, status);
SELECT created_at, amount, status
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC;
- No PostgreSQL o index-only scan precisa de uma tabela com vacuum recente (o visibility map).
- MySQL/InnoDB nao tem a palavra-chave
INCLUDE, mas a chave primaria e anexada automaticamente a cada indice secundario, e um "covering index" se faz listando todas as colunas no proprio indice.
- ClickHouse e diferente: seu indice primario e esparso e nao unico, e o
ORDER BY da tabela define a ordem dos dados em disco.
Quando um composto vence dois indices simples
O planejador pode combinar dois indices separados em user_id e created_at via bitmap, mas isso sao duas leituras mais uma intersecao, mais lento que um unico indice que ja guarda os dados em ordem.
CREATE INDEX idx_orders_user ON orders (user_id);
CREATE INDEX idx_orders_created ON orders (created_at);
- Um indice composto vence quando as colunas sao sempre consultadas juntas em
WHERE/ORDER BY.
- Dois indices simples sao mais flexiveis quando as colunas tambem sao consultadas separadamente.
- Nao exagere nos indices: cada um deixa
INSERT/UPDATE mais lento e ocupa espaco. (a, b) ja serve para consultas em a, entao um indice a parte em a costuma ser redundante.
Um indice composto cobre varias colunas em uma unica estrutura. A ordem certa das colunas transforma filtro, ordenacao e ate a leitura das colunas necessarias em uma unica passagem pelo indice; a ordem errada o deixa quase inutil.
A regra do prefixo a esquerda
Uma B-tree ordena as linhas pelas colunas da esquerda para a direita, como palavras num dicionario. Por isso o indice so e aproveitado a partir da coluna mais a esquerda e sem lacunas.
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at DESC);WHERE user_id = 42funciona, e o prefixo a esquerda.WHERE user_id = 42 AND created_at > now() - interval '7 days'usa as duas colunas.WHERE created_at > now() - interval '7 days'nao consegue usar este indice: a segunda coluna e inalcancavel sem a primeira.Gotcha: a igualdade na primeira coluna "destrava" a ordenacao e os intervalos na segunda. Mas um intervalo na primeira coluna mata a busca pontual na segunda, porque depois de
>ou<as linhas ja nao estao ordenadas pela coluna seguinte.Ordem das colunas: filtro, depois ordenacao, depois intervalo
Regra pratica: primeiro as colunas de igualdade, depois a coluna de ordenacao e por ultimo a coluna de intervalo.
-- "pedidos recentes de um usuario, os novos primeiro" SELECT id, amount, status FROM orders WHERE user_id = 42 AND created_at >= '2026-01-01' ORDER BY created_at DESC LIMIT 20;O indice
(user_id, created_at DESC)e ideal:user_idrestringe por igualdade ecreated_at DESCdevolve as linhas ja ordenadas, entao nao e preciso um passo de ordenacao a parte.=,IN) vem primeiro.ORDER BYvem logo depois das igualdades.>,<,BETWEEN) vem por ultimo, porque quebra a ordem de tudo o que vem depois no indice.Cobrir um ORDER BY e a direcao da ordenacao
Para o indice eliminar o passo de ordenacao, a direcao do
ORDER BYprecisa coincidir com o indice ou ser o seu inverso completo. O PostgreSQL consegue percorrer uma B-tree nos dois sentidos.CREATE INDEX idx_emp_dept_salary ON employees (dept, salary DESC); SELECT name, salary FROM employees WHERE dept = 'eng' ORDER BY salary DESC LIMIT 10;ORDER BY salary ASCtambem funciona sem ordenar, e uma varredura reversa. Mas umORDER BY dept ASC, salary ASCmisto contra um indice(dept, salary DESC)forca uma reordenacao: as direcoes por coluna nao podem ser misturadas a vontade.Colunas INCLUDE e index-only scan
Se o indice contem todas as colunas que a consulta precisa, o motor nao toca na tabela, e um index-only scan. As colunas pelas quais voce nao filtra ficam melhor adicionadas via
INCLUDE: elas vivem so nas folhas e nao incham a navegacao pela arvore.CREATE INDEX idx_orders_user_inc ON orders (user_id, created_at DESC) INCLUDE (amount, status); SELECT created_at, amount, status FROM orders WHERE user_id = 42 ORDER BY created_at DESC;INCLUDE, mas a chave primaria e anexada automaticamente a cada indice secundario, e um "covering index" se faz listando todas as colunas no proprio indice.ORDER BYda tabela define a ordem dos dados em disco.Quando um composto vence dois indices simples
O planejador pode combinar dois indices separados em
user_idecreated_atvia bitmap, mas isso sao duas leituras mais uma intersecao, mais lento que um unico indice que ja guarda os dados em ordem.-- combinacao bitmap de dois indices: funciona, mas nao entrega ordem CREATE INDEX idx_orders_user ON orders (user_id); CREATE INDEX idx_orders_created ON orders (created_at);WHERE/ORDER BY.INSERT/UPDATEmais lento e ocupa espaco.(a, b)ja serve para consultas ema, entao um indice a parte emacostuma ser redundante.