Um índice de banco de dados é uma estrutura de busca ordenada que permite à engine encontrar as linhas certas diretamente, em vez de varrer a tabela inteira. A analogia do livro é exata: ninguém encontra “idempotência” num livro de 900 páginas lendo o livro — a pessoa consulta o índice e pula para a página. Bancos de dados fazem a mesma escolha em cada query, e poder pular ou não é a diferença mais comum entre uma query de 5 milissegundos e uma de 5 segundos.
Principais pontos
| Pergunta | Resposta |
|---|---|
| O que é | Uma estrutura lateral ordenada: valores + ponteiros, como o índice de um livro |
| O ganho | Busca logarítmica em vez de varredura linear — decisivo em escala |
| O imposto | Toda escrita atualiza todo índice; o armazenamento cresce |
| O que indexar | Colunas de filtro, de join/ponteiro e de ordenação — se forem seletivas |
| O diagnóstico | EXPLAIN: um table scan numa tabela grande é o sinal |
O índice, criado e sentido
-- O cavalo de batalha: um índice composto com o formato da query quente
CREATE INDEX orders_pending ON orders (status, created_at);
-- Especializações da mesma ideia:
CREATE UNIQUE INDEX users_email ON users (email); -- constraint + velocidade
CREATE INDEX unshipped ON orders (created_at)
WHERE shipped = false; -- parcial: só o subconjunto quente
-- O antes/depois, pelo plano:
EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > now() - interval '1 day';
-- antes: Seq Scan on orders (linhas examinadas: 4.812.309)
-- depois: Index Scan using orders_pending (linhas examinadas: 1.214)
O código de aplicação encontra a mesma física pela forma da query — esta query é a especificação do índice (status, createdAt), seja qual for a superfície que a escreve:
// JavaScript / Node.js — Back4app JS SDK
// The query shape tells you the index: (status, createdAt)
const query = new Parse.Query('Order');
query.equalTo('status', 'pending'); // equality first…
query.greaterThan('createdAt', since); // …then the range
query.descending('createdAt'); // …sorted by the same column
const backlog = await query.find();
// Indexed: milliseconds at any size. Unindexed: a full collection scan. // Flutter / Dart — Back4app Flutter SDK
// The query shape tells you the index: (status, createdAt)
final query = QueryBuilder<ParseObject>(ParseObject('Order'))
..whereEqualTo('status', 'pending') // equality first…
..whereGreaterThan('createdAt', since) // …then the range
..orderByDescending('createdAt'); // …sorted by the same column
final response = await query.query();
// Indexed: milliseconds at any size. Unindexed: a full collection scan. // iOS / Swift — Back4app Swift SDK
// The query shape tells you the index: (status, createdAt)
let query = Order.query("status" == "pending", "createdAt" > since)
.order([.descending("createdAt")])
query.find { result in
if case .success(let backlog) = result { render(backlog) }
}
// Indexed: milliseconds at any size. Unindexed: a full collection scan. // Android / Kotlin — Back4app Android SDK
// The query shape tells you the index: (status, createdAt)
val query = ParseQuery.getQuery<ParseObject>("Order")
query.whereEqualTo("status", "pending") // equality first…
query.whereGreaterThan("createdAt", since) // …then the range
query.orderByDescending("createdAt") // …sorted by the same column
query.findInBackground { backlog, e -> if (e == null) render(backlog) }
// Indexed: milliseconds at any size. Unindexed: a full collection scan. Como o salto funciona
A estrutura padrão em toda parte é a B-tree: rasa, ordenada, autobalanceada — três ou quatro níveis cobrem centenas de milhões de entradas, e as suas folhas são encadeadas, razão pela qual uma mesma estrutura atende igualdade, ranges e ORDER BY. Essa versatilidade é o motivo de “na dúvida, B-tree” ser o conselho permanente, com as alternativas atuando como especialistas.
B-tree vs. hash vs. os especialistas
Qual tipo de índice você deveria usar?
| Tipo | Atende | Use quando |
|---|---|---|
| B-tree (padrão) | =, <, >, ranges, ordenação, prefixos | Quase sempre — o generalista |
| Hash | Só igualdade, O(1) | Lookups de correspondência exata, nada mais |
| Composto | Multicoluna, prefixo à esquerda | A query quente filtra por várias colunas |
| Único | B-tree + sem duplicados | Constraint e índice em um só |
| Parcial | Uma fatia filtrada de linhas | Subconjuntos quentes: não enviados, não lidos, ativos |
| Covering | Query respondida só pelo índice | Queries de leitura pesada com lista estável de colunas |
| Full-text (invertido) | Palavras → documentos | Caixas de busca |
| Geoespacial | Pontos, regiões, distância | Queries de “perto de mim” |
Duas regras carregam a linha do composto: a regra do prefixo à esquerda — um índice em (a, b, c) atende a, (a, b), (a, b, c), nunca b sozinho — e igualdade primeiro, range/ordenação por último na ordem das colunas (o mnemônico ESR na indexação de bancos de documentos, onde as mesmas B-trees fazem o mesmo trabalho).
Quando indexar — e quando não
Indexe: colunas em filtros WHERE frequentes; chaves de join e campos de ponteiro (algumas engines nunca indexam foreign keys automaticamente — um clássico table scan silencioso); colunas de ORDER BY em caminhos quentes; e sempre com a seletividade em mente — uma coluna de e-mail (milhões de valores distintos) se paga, um flag de status (três valores) em geral não se paga sozinho, embora brilhe liderando um composto com um range atrás. Não indexe: tabelas pequenas que a engine varre mais rápido do que busca; tabelas quentes de escrita além dos poucos índices essenciais; colunas já atendidas pelo prefixo esquerdo de um índice existente; e qualquer coisa para a qual você não consegue nomear uma query — índice sem query é puro imposto de escrita. O ciclo de auditoria: rode EXPLAIN nas queries lentas procurando scans, confira as estatísticas de uso para achar índices mortos, adicione e remova de acordo.
Casos de uso comuns
- A query da lista quente. Filtros de status + data atrás de todo dashboard e feed — o território natural do índice composto.
- Campos de login e lookup. E-mail, username, IDs externos — índices únicos fazendo constraint e velocidade ao mesmo tempo.
- Caminhos de join e ponteiro. Toda foreign key e todo ponteiro que as suas queries percorrem; o cúmplice silencioso do problema N+1 é um deles sem índice.
- Filtros multi-tenant. Compostos liderados pelo tenant — a regra da arquitetura multi-tenant de que todo índice começa com
tenant_id. - Busca e geo. Índices invertidos e espaciais movendo as queries que B-trees não atendem.
Você deveria adicionar esse índice? Matriz de decisão
| Adicione quando… | Pule quando… |
|---|---|
| Uma query frequente filtra ou ordena por essa coluna | Nenhuma query de produção a usa |
| O EXPLAIN mostra scans numa tabela que cresce | A tabela é pequena e vai continuar pequena |
| A coluna é seletiva (muitos valores distintos) | O prefixo de um índice existente já a cobre |
| É chave de join ou campo de ponteiro | A tabela é dominada por escrita e a leitura é rara |
| Uma regra de unicidade precisa ser imposta de todo modo | Você está chutando — meça primeiro |
A disciplina em uma frase: índices são criados em resposta a queries, revisados contra o uso real e removidos sem sentimentalismo.
Limitações e trade-offs
- A escrita paga pela leitura. Cada índice é mais uma estrutura que toda escrita precisa manter — cargas em lote notoriamente rodam mais rápido com os índices derrubados e reconstruídos depois.
- O armazenamento é real. Índices comumente rivalizam com o tamanho da própria tabela; covering indexes, em especial, trocam disco por velocidade.
- Quem decide é o otimizador, não você. Um índice de baixa seletividade pode ser ignorado com razão; estatísticas desatualizadas podem ignorar sem razão um índice bom — o plano é a verdade.
- Erros de ordem neutralizam compostos.
(created_at, status)e(status, created_at)são ferramentas diferentes; a regra do prefixo à esquerda não perdoa nada. - Índice não conserta a query. Wildcards no início, funções sobre colunas e loops N+1 derrotam a indexação por cima — a forma da query vem primeiro.
Índices no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. A gestão de índices mora junto com o schema no dashboard: crie, revise e remova índices por classe — de campo único ou compostos — no armazenamento com MongoDB que as suas queries de SDK realmente acessam, seguindo a mesma lógica de B-tree e ESR acima. A query das abas de código e o índice que a atende são desenhados no mesmo lugar — exatamente onde a disciplina de “indexe o que você consulta” quer morar.
Perguntas frequentes
O que é um índice de banco de dados em termos simples?
Uma estrutura separada e ordenada — geralmente uma B-tree — que guarda valores de coluna mais ponteiros para as suas linhas, exatamente como o índice de um livro guarda termos e números de página. A engine procura o valor na pequena estrutura ordenada e pula direto para as linhas correspondentes, em vez de ler a tabela inteira na esperança de encontrá-las.
Quanto mais rápida é uma query indexada?
A diferença entre trabalho logarítmico e linear: um lookup em B-tree toca um punhado de páginas, tenha a tabela dez mil linhas ou cem milhões, enquanto um table scan lê tudo. Em tamanhos pequenos os dois parecem instantâneos — e é por isso que índices ausentes se escondem em desenvolvimento e detonam em produção, onde a mesma query passa a examinar milhões de linhas.
Índices deixam a escrita mais lenta?
Sim — esse é o imposto. Todo insert, update ou delete numa coluna indexada precisa atualizar também cada índice que a referencia: seis índices numa tabela significam até seis atualizações extras de estrutura por escrita, mais o armazenamento que eles ocupam. Índices são uma compra de velocidade de leitura paga em velocidade de escrita e disco — os deliberados se pagam, os esquecidos só te cobram.
Quais colunas devem ser indexadas?
As colunas que as suas queries realmente usam: filtros (WHERE), chaves de join — incluindo foreign keys e campos de ponteiro, que algumas engines não indexam automaticamente — e colunas de ordenação (ORDER BY). Some o teste de seletividade: uma coluna de e-mail que distingue milhões de linhas merece o índice; um flag booleano que divide a tabela ao meio, em geral, não.
Em que ordem devem ficar as colunas de um índice composto?
Igualdade primeiro, range e ordenação depois — e lembre da regra do prefixo à esquerda: um índice em (a, b, c) atende queries que filtram por a, por a e b, ou pelas três, mas nunca por b sozinho. A formulação da mesma regra nos bancos de documentos é ESR: Equality, Sort, Range. A ordem das colunas é a diferença entre um índice composto funcionar e apenas existir.
Como encontro índices ausentes ou sem uso?
Para os ausentes: rode o plano da query — EXPLAIN — e procure table scans em tabelas grandes; a coluna de filtro de uma query lenta e frequente é a candidata. Para os sem uso: toda engine rastreia estatísticas de uso de índices, e um índice que nenhuma query toca há meses é puro imposto de escrita — remova. O plano e as estatísticas, juntos, são a metodologia inteira.
O que são índices únicos, parciais e covering?
Especializações da mesma estrutura: um índice único impõe a regra de não haver duplicados como constraint enquanto acelera lookups; um índice parcial cobre apenas as linhas que atendem a uma condição — pequeno e rápido para subconjuntos quentes, como pedidos não enviados; um covering index contém todas as colunas de que a query precisa, permitindo que a engine responda só com o índice, sem visitar a tabela.
A indexação funciona igual em bancos de documentos?
Conceitualmente idêntica — B-trees sobre valores de campos — com os mesmos trade-offs e a mesma lógica de prefixo à esquerda sob a regra prática ESR. Bancos de documentos adicionam índices multikey sobre campos de array e variantes geoespaciais, e a regra operacional sobrevive à tradução: indexe o que você consulta, especialmente os campos de ponteiro que joins e includes percorrem.