Aprenda como índices de banco de dados reduzem o tempo de consulta, quando ajudam (e quando atrapalham) e passos práticos para projetar, testar e manter índices em aplicações reais.

Um índice de banco de dados é uma estrutura de busca separada que ajuda o banco a encontrar linhas mais rápido. Não é uma segunda cópia da sua tabela. Pense nisso como as páginas de índice de um livro: você usa o índice para ir perto do lugar certo e então lê a página exata (linha) que precisa.
Sem um índice, o banco muitas vezes tem apenas uma opção segura: ler muitas linhas para verificar quais batem com a sua consulta. Isso pode ser aceitável quando a tabela tem alguns milhares de linhas. À medida que a tabela cresce para milhões de linhas, “ver mais linhas” vira mais leituras de disco, mais pressão de memória e mais trabalho da CPU — então a mesma consulta que antes parecia instantânea começa a ficar lenta.
Índices reduzem a quantidade de dados que o banco precisa inspecionar para responder perguntas como “encontre o pedido com ID 123” ou “busque usuários com este email”. Em vez de escanear tudo, o banco segue uma estrutura compacta que estreita rapidamente a busca.
Mas indexação não é uma solução universal. Algumas consultas ainda precisam processar muitas linhas (relatórios amplos, filtros de baixa seletividade, agregações pesadas). E índices têm custos reais: armazenamento extra e gravações mais lentas, porque inserts e updates também precisam atualizar o índice.
Você verá:
Quando um banco executa uma consulta, tem duas opções amplas: escanear a tabela inteira linha a linha, ou pular diretamente para as linhas que batem. A maior parte dos ganhos de indexação vem de evitar leituras desnecessárias.
Uma varredura completa da tabela é exatamente o que parece: o banco lê cada linha, verifica se ela satisfaz a condição WHERE e só então retorna resultados. Isso é aceitável para tabelas pequenas, mas fica mais lento de forma previsível à medida que a tabela cresce — mais linhas significa mais trabalho.
Usando um índice, o banco pode frequentemente evitar ler a maioria das linhas. Em vez disso, consulta primeiro o índice (uma estrutura compacta feita para busca) para encontrar onde as linhas correspondentes estão e depois lê apenas essas linhas específicas.
Pense em um livro. Se quiser cada página que mencione “fotossíntese”, você poderia ler o livro inteiro (varredura completa). Ou poderia usar o índice do livro, pular para as páginas listadas e ler apenas essas seções (busca por índice). A segunda abordagem é mais rápida porque você pula quase todas as páginas.
Bancos gastam muito tempo esperando por leituras — especialmente quando os dados não estão em memória. Diminuir o número de linhas (e páginas) que o banco precisa tocar normalmente reduz:
Indexação ajuda mais quando os dados são grandes e o padrão de consulta é seletivo (por exemplo, buscar 20 linhas correspondentes em 10 milhões). Se sua consulta já retorna a maior parte das linhas, ou a tabela cabe confortavelmente em memória, uma varredura completa pode ser tão rápida — ou até mais rápida.
Índices funcionam porque organizam valores para que o banco possa pular perto do que você quer em vez de checar cada linha.
A estrutura de índice mais comum em bancos SQL é a B-tree (ou “B+tree”). Conceitualmente:
Por ser ordenada, uma B-tree é ótima para tanto buscas de igualdade (WHERE email = ...) quanto consultas por intervalo (WHERE created_at >= ... AND created_at < ...). O banco navega até a vizinhança certa de valores e então varre para frente em ordem.
Dizem que buscas em B-tree são “logarítmicas”. Na prática, isso significa: conforme sua tabela cresce de milhares para milhões de linhas, o número de passos para encontrar um valor cresce devagar, não proporcionalmente.
Em vez de “o dobro de dados significa o dobro do trabalho”, é mais como “muito mais dados significa apenas alguns passos extras de navegação”, porque o banco segue ponteiros por um pequeno número de níveis na árvore.
Alguns motores também oferecem índices hash. Eles podem ser muito rápidos para checagens de igualdade exata porque o valor é transformado em um hash e usado para localizar a entrada diretamente.
A troca: índices hash geralmente não ajudam com intervalos ou leituras ordenadas, e disponibilidade/comportamento variam entre bancos.
PostgreSQL, MySQL/InnoDB, SQL Server e outros armazenam e usam índices de formas distintas (tamanho de página, clustering, colunas incluídas, verificações de visibilidade). Mas o conceito central é o mesmo: índices criam uma estrutura compacta e navegável que permite localizar linhas correspondentes com muito menos trabalho do que escanear a tabela inteira.
Índices não aceleram “SQL” em geral — aceleram padrões de acesso específicos. Quando um índice casa com a forma como sua consulta filtra, junta ou ordena, o banco pode pular direto para as linhas relevantes em vez de ler a tabela toda.
1) Filtros WHERE (especialmente em colunas seletivas)
Se sua consulta frequentemente reduz uma tabela grande para um pequeno conjunto de linhas, um índice é geralmente o primeiro lugar a procurar. Um exemplo clássico é buscar um usuário por identificador.
Sem um índice em users.email, o banco pode ter de escanear cada linha:
SELECT * FROM users WHERE email = '[email protected]';
Com um índice em email, ele localiza rapidamente a(s) linha(s) correspondente(s) e para.
2) Chaves de JOIN (foreign keys e chaves referenciadas)
Joins são onde “pequenas ineficiências” viram grandes custos. Se você une orders.user_id com users.id, indexar as colunas de join (tipicamente orders.user_id e a chave primária users.id) ajuda o banco a casar linhas sem escaneamentos repetidos.
3) ORDER BY (quando você quer resultados já ordenados)
Ordenar é caro quando o banco precisa coletar muitas linhas e ordená‑las depois. Se você frequentemente roda:
SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC;
um índice que alinhe user_id e a coluna de ordenação pode permitir ao engine ler as linhas na ordem necessária em vez de ordenar um grande resultado intermediário.
4) GROUP BY (quando o agrupamento alinha com um índice)
Agrupar pode se beneficiar quando o banco consegue ler os dados em ordem agrupada. Não é garantia, mas se você costuma agrupar por uma coluna que também é usada para filtrar (ou é naturalmente agrupada no índice), o engine pode fazer menos trabalho.
Índices B-tree são especialmente bons para condições de intervalo — pense em datas, preços e consultas “entre”:
SELECT * FROM orders
WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01';
Para dashboards, relatórios e telas de “atividade recente”, esse padrão é onipresente, e um índice na coluna de intervalo costuma trazer melhoria imediata.
O tema é simples: índices ajudam quando espelham como você busca e ordena. Se suas consultas se alinham com esses padrões de acesso, o banco faz leituras direcionadas em vez de varreduras amplas.
Um índice ajuda principalmente quando reduz bastante o número de linhas que o banco precisa tocar. Essa propriedade chama‑se seletividade.
Seletividade é basicamente: quantas linhas batem para um dado valor? Uma coluna altamente seletiva tem muitos valores distintos, então cada busca bate em poucas linhas.
email, user_id, order_number (frequentemente únicos ou quase)is_active, is_deleted, status com poucos valores comunsCom alta seletividade, um índice pula direto para um pequeno conjunto de linhas. Com baixa seletividade, o índice aponta para um grande trecho da tabela — então o banco ainda tem de ler e filtrar muito.
Considere uma tabela com 10 milhões de linhas e uma coluna is_deleted onde 98% são false. Um índice em is_deleted não ajuda muito para:
SELECT * FROM orders WHERE is_deleted = false;
O conjunto de correspondência ainda é quase a tabela inteira. Usar o índice pode até ser mais lento que uma varredura sequencial porque o engine faz trabalho extra pulando entre entradas do índice e páginas da tabela.
Os planejadores estimam custos. Se um índice não reduzir o trabalho o suficiente — porque muitas linhas batem, ou porque a consulta também precisa da maior parte das colunas — eles podem escolher uma varredura completa.
Distribuição de dados não é fixa. Uma coluna status pode começar bem distribuída e depois derivar para que um valor domine. Se estatísticas não forem atualizadas, o planejador pode tomar decisões ruins, e um índice que antes ajudava pode parar de compensar.
Índices de coluna única são um bom começo, mas muitas consultas reais filtram por uma coluna e ordenam ou filtram por outra. É aí que índices compostos (multi‑coluna) brilham: um índice pode servir várias partes da consulta.
A maioria dos bancos (especialmente com B-tree) só consegue usar um índice composto de forma eficiente a partir das colunas mais à esquerda. Pense no índice como ordenado primeiro por A, depois por B, e assim por diante.
Isso significa:
account_id e então ordenam/filtram por created_atcreated_at (porque não é a coluna mais à esquerda)Uma carga comum é “mostre os eventos mais recentes para esta conta”. Esse padrão:
SELECT id, created_at, type
FROM events
WHERE account_id = ?
ORDER BY created_at DESC
LIMIT 50;
frequentemente se beneficia muito de:
CREATE INDEX events_account_created_at
ON events (account_id, created_at);
O banco pula direto para a porção do índice daquela conta e lê em ordem temporal, em vez de escanear e ordenar um conjunto grande.
Um índice cobridor contém todas as colunas que a consulta precisa, de modo que o banco pode retornar resultados apenas a partir do índice sem olhar as linhas da tabela (menos leituras, menos I/O randômico).
Cuidado: adicionar colunas extras pode tornar o índice grande e caro.
Índices compostos largos podem desacelerar gravações e consumir muito armazenamento. Adicione‑os apenas para consultas específicas de alto valor e verifique com um plano EXPLAIN e medições reais antes e depois.
Índices são muitas vezes descritos como “velocidade grátis”, mas não são. Estruturas de índice precisam ser mantidas sempre que a tabela muda, e consomem recursos reais.
Quando você INSERT uma nova linha, o banco não escreve apenas a linha — também insere entradas correspondentes em cada índice dessa tabela. O mesmo vale para DELETE e muitos UPDATEs.
Por isso “mais índices” podem desacelerar cargas com muitas gravações. Um UPDATE que altera uma coluna indexada pode ser especialmente caro: o banco pode ter de remover a antiga entrada do índice e adicionar a nova (e em algumas engines isso pode disparar splits de página ou rebalanceamentos internos). Se seu app faz muitas gravações — eventos de pedido, dados de sensores, logs de auditoria — indexar tudo pode deixar o banco lento mesmo com leituras rápidas.
Cada índice ocupa espaço em disco. Em tabelas grandes, índices podem rivalizar (ou exceder) o tamanho da própria tabela, especialmente se houver vários índices sobrepostos.
Isso também afeta memória. Bancos dependem fortemente de caching; se seu working set inclui vários índices grandes, o cache precisa manter mais páginas para continuar rápido. Caso contrário, você verá mais I/O em disco e desempenho menos previsível.
Indexar é escolher o que acelerar. Se a sua carga é de leitura, mais índices podem valer a pena. Se é de escrita, priorize índices que suportem suas consultas mais importantes e evite duplicatas. Uma regra útil: adicione um índice só quando você conseguir nomear a consulta que ele ajuda — e verifique que o ganho na leitura compensa o custo de escrita e manutenção.
Adicionar um índice parece que deve ajudar — mas você pode (e deve) verificar. As duas ferramentas que tornam isso concreto são o plano de consulta (EXPLAIN) e medições reais antes/depois.
Execute EXPLAIN (ou EXPLAIN ANALYZE) na consulta exata que importa.
EXPLAIN ANALYZE): se o plano estimou 100 linhas mas tocou na prática 100.000, o otimizador errou — muitas vezes porque as estatísticas estão desatualizadas ou o filtro é menos seletivo que o esperado.ORDER BY, esse sort pode desaparecer, o que é um grande ganho.Faça benchmark da consulta com os mesmos parâmetros, em dados representativos, e capture latência (p50/p95) e linhas processadas.
Cuidado com cache: a primeira execução pode ser mais lenta porque os dados não estão em memória; execuções repetidas podem parecer “resolvidas” mesmo sem índice. Para não se enganar, compare várias execuções e foque em mudanças de plano (índice usado, menos linhas lidas) além do tempo bruto.
Se EXPLAIN ANALYZE mostra menos linhas tocadas e menos passos caros (como sorts), você provou que o índice ajuda — não apenas torceu para que ajudasse.
Você pode adicionar o “índice certo” e ainda assim não ver melhoria se a consulta estiver escrita de forma que impeça o banco de usá‑lo. Esses problemas são sutis, porque a consulta continua correta — só que forçada para um plano mais lento.
1) Wildcards no início
Quando você escreve:
WHERE name LIKE '%term'
o banco não consegue usar um índice B-tree normal para pular ao ponto certo, porque não sabe onde em ordem começaria “%term”. Frequentemente cai numa varredura ampla.
Alternativas:
WHERE name LIKE 'term%'.2) Funções em colunas indexadas
Isso parece inofensivo:
WHERE LOWER(email) = '[email protected]'
Mas LOWER(email) altera a expressão, então o índice em email não pode ser usado diretamente.
Alternativas:
WHERE email = ....LOWER(email).Conversões implícitas de tipo: comparar tipos diferentes pode obrigar o banco a castar um lado, o que pode desabilitar o índice. Exemplo: comparar uma coluna integer com uma literal string.
Collations/encodings inconsistentes: se a comparação usa uma collation diferente da que o índice foi criado (comum em texto entre localidades), o otimizador pode evitar o índice.
LIKE '%x')?LOWER(col), DATE(col), CAST(col)) ?EXPLAIN para confirmar a escolha do banco?Índices não são “coloque e esqueça”. Com o tempo, dados mudam, padrões de consulta mudam e a forma física de tabelas e índices se distancia do ideal. Um índice bem escolhido pode ficar menos efetivo — ou até prejudicial — se não for mantido.
A maioria dos bancos usa um planejador para escolher como rodar uma consulta: qual índice usar, ordem de joins, se uma busca por índice vale a pena. Para isso, o planejador usa estatísticas — resumos sobre distribuição de valores, contagem de linhas e skew.
Quando estatísticas estão velhas, as estimativas de linhas do planejador podem estar muito erradas. Isso leva a escolhas ruins de plano, como escolher um índice que retorna muito mais linhas que o esperado ou pular um índice que seria mais rápido.
Correção rotineira: agende atualizações regulares de estatísticas (geralmente ANALYZE ou similar). Após grandes cargas, deletes massivos ou churn significativo, atualize as estatísticas mais cedo.
À medida que linhas são inseridas, atualizadas e deletadas, índices podem acumular bloat (páginas extras sem dados úteis) e fragmentação (dados espalhados que aumentam I/O). O resultado é índices maiores e mais leituras, especialmente para consultas por intervalo.
Correção rotineira: periodicamente reconstrua ou reorganize índices muito usados quando crescerem desproporcionalmente ou quando o desempenho degradar. Ferramentas e impacto variam por banco; trate isso como uma operação medida, não uma regra cega.
Monitore:
Esse feedback ajuda a detectar quando manutenção é necessária — e quando um índice deve ser ajustado ou removido. Para mais sobre validar melhorias, veja /blog/how-to-prove-an-index-helps-explain-and-measurements.
Adicionar um índice deve ser uma mudança deliberada, não um palpite. Um fluxo leve mantém o foco em ganhos mensuráveis e evita “proliferação de índices”.
Comece com evidência: logs de consultas lentas, traços APM ou relatos de usuários. Escolha uma consulta que seja lenta e frequente — um relatório raro de 10s importa menos que uma busca comum de 200 ms.
Capture o SQL exato e o padrão de parâmetros (por exemplo: WHERE user_id = ? AND status = ? ORDER BY created_at DESC LIMIT 50). Pequenas diferenças mudam qual índice ajuda.
Registre latência atual (p50/p95), linhas escaneadas e impacto CPU/IO. Salve o plano atual (EXPLAIN / EXPLAIN ANALYZE) para comparar depois.
Escolha colunas que correspondam a como a consulta filtra e ordena. Prefira o índice mínimo que faça o plano parar de escanear grandes ranges.
Teste em staging com volume de dados parecido com produção. Índices podem parecer ótimos em datasets pequenos e desapontar em escala.
Em tabelas grandes, use opções online quando suportadas (por exemplo, PostgreSQL CREATE INDEX CONCURRENTLY). Agende mudanças em horários de menor tráfego se o banco puder bloquear gravações.
Rerun a mesma consulta e compare:
Se o índice aumentar o custo de gravação ou lotar memória, remova‑o limpo (por ex., DROP INDEX CONCURRENTLY onde disponível). Mantenha a migração reversível.
Na migração ou nas notas de esquema, escreva qual consulta o índice serve e qual métrica melhorou. O futuro você (ou um colega) saberá por que ele existe e quando é seguro deletar.
Se você está construindo um serviço novo e quer evitar “proliferação de índices” cedo, o Koder.ai pode ajudar a iterar mais rápido no ciclo completo: gerar um app React + Go + PostgreSQL a partir do chat, ajustar migrations de schema/índices conforme requisitos mudam e exportar o código quando quiser assumir manualmente. Na prática, isso facilita ir de “este endpoint está lento” para “aqui está o plano EXPLAIN, o índice mínimo e uma migration reversível” sem esperar por um pipeline tradicional completo.
Índices são uma alavanca enorme, mas não são um botão mágico de “deixe rápido”. Às vezes a parte lenta de uma requisição acontece após o banco encontrar as linhas certas — ou seu padrão de consulta faz da indexação a escolha errada.
Se sua consulta já usa um bom índice mas ainda está lenta, procure por:
OFFSET 999000 pode ser lento mesmo com índices. Prefira paginação por keyset (ex.: “me dê linhas após o último id/timestamp visto”).SELECT *) ou retornar dezenas de milhares de registros pode tornar-se gargalo na rede, serialização JSON ou processamento da aplicação.Se quiser um método mais profundo para diagnosticar gargalos, combine isso com o fluxo em /blog/how-to-prove-an-index-helps.
Não chute. Meça onde o tempo é gasto (execução do banco vs. linhas retornadas vs. código da aplicação). Se o banco está rápido mas a API está lenta, mais índices não vão ajudar.
Uma estrutura separada de dados (frequentemente uma B-tree) que armazena valores de colunas selecionadas em forma ordenada e pesquisável com ponteiros de volta para as linhas da tabela. O banco de dados a usa para evitar ler a maior parte da tabela ao responder consultas seletivas.
Não é uma segunda cópia completa da tabela, mas duplica alguns dados de coluna e metadados, por isso consome espaço extra.
Sem um índice, o banco pode ter de fazer uma varredura completa da tabela: ler muitas (ou todas) as linhas e verificar cada uma contra a cláusula WHERE.
Com um índice, ele frequentemente consegue pular diretamente para as localizações das linhas que batem e ler apenas essas linhas, reduzindo I/O de disco, trabalho de CPU para filtrar e pressão sobre o cache.
Uma B-tree mantém os valores ordenados e organizados em páginas que apontam para outras páginas, então o banco consegue navegar rapidamente até a “vizinhança” correta de valores.
Por isso B-trees funcionam bem tanto para:
WHERE email = ...)WHERE created_at >= ... AND created_at < ...)Índices hash podem ser muito rápidos para igualdade exata (=) porque transformam o valor em um hash e vão diretamente ao bucket.
Trocas:
Em muitas cargas reais, B-trees são o padrão porque suportam mais padrões de consulta.
Geralmente ajudam mais para:
WHERE seletivos (poucas linhas batem)JOIN (foreign keys e chaves referenciadas)ORDER BY que coincide com a ordem do índice (pode evitar um sort)GROUP BY quando a leitura em ordem agrupada reduz o trabalhoSeletividade é “quantas linhas batem para um dado valor”. Índices compensam quando um predicado reduz muito a tabela para um pequeno conjunto de resultados.
Colunas de baixa seletividade (por exemplo, is_deleted, is_active, enums pequenos) frequentemente batem em uma grande parte da tabela. Nesses casos, usar o índice pode ser mais lento que uma varredura porque o engine ainda precisa buscar e filtrar muitas linhas.
Porque o otimizador estima que usá‑lo não reduzirá trabalho suficiente.
Razões comuns:
Na maioria das implementações B-tree, o índice é efetivamente ordenado pela primeira coluna, depois pela segunda, etc. Assim, o banco usa o índice de forma eficiente a partir das colunas mais à esquerda.
Exemplo:
(account_id, created_at) é ótimo para WHERE account_id = ? com filtragem/ordenação por tempo.created_at (pois não é a coluna mais à esquerda).Um índice cobridor inclui todas as colunas necessárias pela consulta, então o banco pode retornar resultados só a partir do índice sem consultar as linhas da tabela.
Benefícios:
Custos:
Use índices cobridores para consultas específicas de alto valor, não “por via das dúvidas”.
Cheque duas coisas:
EXPLAIN / EXPLAIN ANALYZE e confirme se o plano mudou (por exemplo, Seq Scan → Index Scan/Seek, menos linhas lidas, passo de sort removido).Se a consulta retorna uma grande fração da tabela, o ganho costuma ser pequeno.
Também observe o impacto nas gravações, já que novos índices podem desacelerar INSERT/UPDATE/DELETE.