O que significa “lentidão com índices” na prática
Uma consulta pode estar lenta mesmo existindo índices porque: (1) o índice não combina com os predicados (filtros) reais, (2) o plano escolhe um caminho que lê dados demais, (3) há ordenação/paginação custosa, (4) joins multiplicam linhas antes de filtrar, (5) funções/conversões impedem o uso eficiente do índice, (6) a consulta retorna mais dados do que o necessário, (7) o volume cresceu e o que “funcionava” deixou de funcionar. O objetivo deste roteiro é sair do sintoma (“está lento”) e chegar a uma hipótese testável (“este índice/reescrita reduz leituras e tempo”).
Roteiro prático e repetível: do sintoma à causa
1) Identifique a consulta problemática (a “top 1” real)
Antes de mexer em índices, garanta que você está olhando para a consulta certa: a mais lenta, a mais frequente, ou a que mais consome recursos no agregado. Capture o SQL exato (com parâmetros) e o contexto: endpoint/tela, horário, usuário, volume típico.
- Checklist: a consulta é executada muitas vezes? é lenta só em alguns horários? mudou após deploy/migração? depende de parâmetros específicos?
- Armadilha comum: otimizar uma consulta rara e ignorar uma consulta “média” que roda milhares de vezes.
2) Meça tempo e variabilidade (não confie em uma única execução)
Meça latência com repetição e registre dispersão. O que importa é: média/mediana e p95/p99 (cauda). Variabilidade alta costuma indicar concorrência, cache, I/O intermitente, ou planos diferentes por parâmetros.
- Execute a mesma consulta várias vezes com os mesmos parâmetros e registre tempos.
- Separe “primeira execução” (cache frio) das seguintes (cache quente), se isso for relevante ao seu cenário.
- Registre também: quantidade de linhas retornadas e tamanho aproximado do payload.
Template de medição (planilha ou log): SQL_id | parâmetros | data/hora | exec# | tempo_ms | linhas_retornadas | observações3) Capture o plano conceitual e os gargalos dominantes
Com o plano em mãos, descreva em linguagem simples o que está acontecendo: qual tabela é lida primeiro, como os joins são feitos, onde ocorre filtro, onde ocorre ordenação, e onde há operações caras (ex.: varredura ampla, sort grande, hash join com muita memória, nested loop com muitas iterações).
- Objetivo: localizar o “maior bloco de custo” (onde mais se lê/ordena/itera).
- Regra prática: se o plano mostra leitura de muitas linhas para retornar poucas, o problema quase sempre é filtro/índice/ordem de execução.
4) Verifique seletividade real dos filtros (na prática, não no “achismo”)
Liste os predicados do WHERE e estime quantas linhas cada um deixa passar. O filtro “mais seletivo” (que reduz mais) é o candidato a guiar o acesso. Se nenhum filtro é seletivo, índice pode não ajudar tanto, e talvez o problema seja retorno grande, ordenação, ou necessidade de agregação.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Conte o total de linhas da tabela e compare com o número de linhas após aplicar cada filtro (mesmo que com amostras).
- Procure predicados que parecem seletivos, mas não são (ex.: status com poucos valores, flags booleanas, colunas com muitos nulos).
Perguntas-template (filtros): 1) Qual predicado filtra mais? 2) Algum predicado é quase sempre verdadeiro? 3) Há IN com muitos valores? 4) Há filtros por intervalo (datas) muito amplos?5) Procure “bloqueadores de índice” nos predicados
Mesmo com índice na coluna, certos padrões impedem ou degradam o uso eficiente:
- Funções na coluna filtrada:
WHERE DATE(created_at) = '2026-01-25'tende a impedir range scan direto. Prefira intervalos:created_at >= ... AND created_at < .... - Conversões implícitas: comparar texto com número (ou vice-versa) pode forçar conversão na coluna.
- Predicados não sargáveis:
LIKE '%abc',col + 1 = 10,COALESCE(col,0)=...(dependendo do SGBD). - OR amplo: pode levar a planos que evitam índice ou fazem múltiplos acessos caros.
Perguntas-template (sargabilidade): 1) Há funções na coluna filtrada? 2) Há casts/conversões? 3) O LIKE tem coringa no início? 4) O OR poderia ser reescrito (UNION ALL, predicados equivalentes)?6) Avalie joins: cardinalidade, ordem e colunas de junção
Joins lentos geralmente vêm de: (1) juntar cedo demais e filtrar tarde, (2) chaves de junção sem suporte de índice no lado “muitos”, (3) estimativas erradas que escolhem algoritmo de join inadequado, (4) multiplicação de linhas por relacionamento 1:N antes de limitar/ordenar.
- Identifique a tabela “dirigente” (a que deveria ser reduzida primeiro pelos filtros).
- Verifique se as colunas usadas no
ONtêm suporte de índice no lado com maior volume. - Observe se o plano faz nested loop com muitas iterações (sinal de acesso repetido caro).
Perguntas-template (joins): 1) Qual join aumenta mais o número de linhas intermediárias? 2) O filtro está sendo aplicado antes ou depois do join? 3) Há join em coluna com baixa seletividade? 4) Há join com tipos diferentes (int vs varchar)?7) Cheque ORDER BY, GROUP BY, DISTINCT e paginação
Ordenação e deduplicação podem dominar o custo, especialmente quando o conjunto intermediário é grande. Paginação mal feita (offset alto) pode forçar o banco a ordenar e descartar muitas linhas.
- ORDER BY sem suporte: se o banco precisa ordenar um grande conjunto, um índice alinhado ao
WHERE+ORDER BYpode reduzir ou eliminar o sort. - OFFSET alto:
LIMIT 50 OFFSET 500000tende a piorar com o tempo. Considere paginação por “seek” (keyset) quando possível. - DISTINCT/GROUP BY: verifique se está sendo usado para “consertar” duplicatas de join; isso pode esconder um problema de modelagem/consulta.
Perguntas-template (ordenação/paginação): 1) Há ORDER BY sem suporte? 2) O ORDER BY usa colunas diferentes do filtro principal? 3) O OFFSET cresce muito? 4) DISTINCT está mascarando duplicação de join?8) Considere volume de dados e “tamanho do retorno”
Às vezes a consulta é lenta porque retorna dados demais (muitas linhas ou colunas grandes). Índice não compensa tráfego e materialização excessiva.
- Reduza colunas: evite
SELECT *em tabelas largas. - Confirme se o número de linhas retornadas é realmente necessário.
- Observe colunas grandes (text/blob/json) que podem custar caro para ler/transferir.
Perguntas-template (retorno): 1) Há retornos grandes demais? 2) SELECT * é necessário? 3) Há colunas grandes sendo trazidas sem uso? 4) A aplicação poderia paginar ou limitar melhor?Propondo hipóteses de índices (sem “atirar no escuro”)
Com os passos anteriores, formule hipóteses específicas e verificáveis. Cada hipótese deve dizer: qual parte do plano melhora (filtro, join, ordenação), qual acesso será reduzido (linhas lidas, loops, sort), e qual índice (ou ajuste) atende isso.
Modelo de hipótese
| Observação no plano/sintoma | Hipótese | Intervenção | Métrica esperada |
|---|---|---|---|
Varredura ampla com filtro seletivo em status + created_at | Falta índice que comece pelo filtro mais seletivo e permita range por data | Criar índice composto alinhado ao WHERE | Menos linhas lidas; queda de tempo e I/O |
Sort grande por created_at DESC após filtrar | ORDER BY não está suportado pelo caminho de acesso | Índice que suporte filtro + ordenação | Sort reduzido/eliminado; menor memória/tempo |
| Nested loop com muitas iterações em join 1:N | Falta índice na FK do lado N | Índice na coluna de junção do lado N | Menos custo por iteração; queda de tempo |
Receitas comuns (adaptar ao seu caso)
- Filtro seletivo + ordenação: índice que combine colunas do
WHEREe doORDER BYna ordem que o acesso precisa. - Join em tabela grande: índice na coluna de junção do lado com mais linhas (frequentemente a FK).
- Paginação por seek: índice que suporte o predicado de “continuação” (ex.:
created_ate um desempate comoid). - Evitar função na coluna: reescrever predicado para intervalo e então indexar a coluna “crua”.
Validando melhorias: testes controlados e comparação antes/depois
1) Defina o que será comparado
- Mesma consulta, mesmos parâmetros.
- Mesmo volume de dados (ou snapshot).
- Mesmas condições de carga (idealmente ambiente de teste com carga reproduzível).
- Mesmas configurações relevantes (cache, estatísticas, parâmetros do otimizador).
2) Colete métricas antes e depois
Não compare só “tempo total”. Compare também indicadores que explicam o porquê:
- Tempo (mediana e p95/p99).
- Linhas lidas vs linhas retornadas.
- Quantidade de leituras lógicas/físicas (se disponível).
- Tempo de CPU e tempo de espera por I/O (se disponível).
- Presença/ausência de sort grande e tamanho de intermediários.
3) Execute em série e observe estabilidade
Rode múltiplas execuções e compare distribuição. Uma melhoria real tende a reduzir não só a média, mas também a variabilidade (menos “picos”).
Template de experimento: Baseline: 20 execuções (cache quente) + 5 (cache frio) Intervenção: criar índice X / reescrever predicado Y Repetir: mesmas execuções, mesmos parâmetros Comparar: mediana, p95, linhas lidas, presença de sort, loops de join4) Verifique efeitos colaterais
- Impacto em escrita: INSERT/UPDATE/DELETE na tabela indexada.
- Impacto em outras consultas: o novo índice pode mudar planos (para melhor ou pior).
- Espaço e manutenção: tamanho do índice e custo de rebuild/auto-maintenance.
Templates de perguntas para diagnóstico rápido (copiar e usar)
Sobre filtros
- Qual predicado filtra mais?
- Há predicados pouco seletivos dominando o acesso?
- Há funções na coluna filtrada?
- Há conversões implícitas de tipo?
Sobre joins
- Qual join aumenta mais o conjunto intermediário?
- O filtro está acontecendo antes do join?
- Existe índice nas colunas do ON no lado “muitos”?
- O join está causando nested loop com muitas iterações?
Sobre ordenação e paginação
- Há ORDER BY sem suporte?
- O ORDER BY está alinhado com o filtro principal?
- O OFFSET é alto e cresce com o tempo?
- DISTINCT/GROUP BY está sendo usado para corrigir duplicatas de join?
Sobre retorno e volume
- Há retornos grandes demais?
- SELECT * é necessário?
- Há colunas grandes sendo trazidas sem uso?
- O volume de dados cresceu e mudou a seletividade?
Critérios de sucesso e riscos a monitorar
Critérios de sucesso (objetivos mensuráveis)
- Redução consistente de latência (mediana e p95) sob condições equivalentes.
- Redução de linhas lidas/varridas para chegar no mesmo resultado.
- Eliminação ou redução significativa de sort/temporários grandes.
- Plano mais estável para os parâmetros mais comuns (menos variação inesperada).
Riscos e trade-offs (especialmente regressões em escrita)
- Queda de performance em INSERT/UPDATE/DELETE devido ao novo índice.
- Regressão em consultas diferentes que passam a escolher um plano pior.
- Índice “quase certo” que ajuda um parâmetro e piora outro (sensibilidade a parâmetros).
- Aumento de espaço e tempo de manutenção (rebuild, vacuum/analyze, etc.).