DevGuideTK
← Trilha Backend

Índices

A estrutura que faz uma consulta cair de segundos para milissegundos, e o que ela custa na escrita.

Os planos de execução desta página foram gerados no PostgreSQL 17, numa tabela de pedidos com dois milhões de linhas. Os tempos variam de máquina para máquina; a forma dos planos é o que importa.

Ler um plano de execução

EXPLAIN mostra o plano que o banco escolheu. EXPLAIN ANALYZE executa a consulta de verdade e acrescenta os números reais.

EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 4242;

Sem índice:

Gather  (cost=1000.00..33640.77 rows=21 width=57)
        (actual time=13.337..77.391 rows=22 loops=1)
  Workers Planned: 2
  ->  Parallel Seq Scan on pedidos
        Filter: (cliente_id = 4242)
        Rows Removed by Filter: 666659
Execution Time: 77.411 ms

Com o índice em cliente_id:

Bitmap Heap Scan on pedidos  (cost=4.59..86.92 rows=21 width=57)
                             (actual time=0.055..0.095 rows=22 loops=1)
  Recheck Cond: (cliente_id = 4242)
  ->  Bitmap Index Scan on idx_pedidos_cliente
        Index Cond: (cliente_id = 4242)
Execution Time: 0.119 ms

O que olhar em cada plano:

ItemO que significaSinal de alerta
Seq ScanLeitura da tabela inteiraEm tabela grande, numa consulta que retorna poucas linhas
Index ScanPercorre o índice e busca cada linha na tabelaNormal e desejado
Bitmap Index ScanMarca as posições pelo índice e lê a tabela em ordem físicaNormal quando há várias linhas
Index Only ScanResponde só com o índiceO melhor caso
Rows Removed by FilterLinhas lidas e descartadasNúmero grande: falta índice ou o índice não serve
rows= estimado e realLinhas previstas pelo planejador e encontradasDiferença grande: estatísticas desatualizadas; rode ANALYZE
SortOrdenação feita depois da leituraEm consultas com ORDER BY ... LIMIT, um índice na ordem certa elimina essa etapa

O índice composto eliminando a ordenação

Para a tela de últimos pedidos do cliente, com só o índice em cliente_id, o banco busca todos os pedidos do cliente e ordena:

Limit
  ->  Sort
        Sort Key: criado_em DESC
        ->  Bitmap Heap Scan on pedidos
              ->  Bitmap Index Scan on idx_pedidos_cliente

Com o índice (cliente_id, criado_em DESC), a ordenação some. O banco lê os 20 primeiros já na ordem e para:

Limit
  ->  Index Scan using idx_pedidos_cliente_data on pedidos
        Index Cond: (cliente_id = 4242)

Com 22 pedidos por cliente, a diferença é pequena. Com um cliente que tem 50 mil pedidos, a primeira versão ordena 50 mil linhas para devolver 20.

Criar índices em produção

CREATE INDEX bloqueia as gravações na tabela enquanto o índice é construído. Numa tabela grande, isso pode levar minutos com o sistema parado. O PostgreSQL oferece uma forma que não bloqueia:

CREATE INDEX CONCURRENTLY idx_pedidos_cliente_data
  ON pedidos (cliente_id, criado_em DESC);
CREATE INDEXCREATE INDEX CONCURRENTLY
Bloqueia gravaçõesSim, até terminarNão
TempoMenorMaior: lê a tabela duas vezes
Dentro de uma transaçãoPodeNão pode
Se falharDesfaz tudoDeixa um índice inválido, que precisa ser removido

Muitas ferramentas de migração rodam cada migração dentro de uma transação. Para usar CONCURRENTLY, é preciso desligar esse comportamento naquela migração. No MySQL, a criação de índice no InnoDB já é feita sem bloquear as gravações na maioria dos casos.

Uma consulta paginada que usa o índice

A tela "meus pedidos" com paginação é o caso mais comum de índice composto. Os exemplos buscam a próxima página a partir da data do último pedido exibido (paginação por cursor), o que usa o índice (cliente_id, criado_em DESC) do começo ao fim, em qualquer página.

CREATE INDEX idx_pedidos_cliente_data ON pedidos (cliente_id, criado_em DESC);
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

export async function pedidosDoCliente(clienteId: number, antesDe: Date, limite = 20) {
  const { rows } = await pool.query(
    `SELECT id, status, criado_em
       FROM pedidos
      WHERE cliente_id = $1
        AND criado_em < $2
      ORDER BY criado_em DESC
      LIMIT $3`,
    [clienteId, antesDe, limite],
  );
  return rows;
}

O que muda de uma linguagem para outra

A consulta é a mesma; o que muda é como a data viaja até o banco, e é aí que um índice pode deixar de ser usado.

Tipo da data no códigoCuidado
NodeDateÉ convertido para timestamptz com o fuso; não envie a data como texto formatado à mão
PythondatetimeUse datetime com fuso (timezone.utc); sem fuso, a comparação usa o fuso da sessão
Gotime.TimeEnviado com fuso; time.Time{} vazio vira o ano 1 e retorna tudo
JavaOffsetDateTimeUse setObject; java.util.Date e Timestamp perdem a informação de fuso
C#DateTimePara colunas timestamptz, o Npgsql exige DateTimeKind.Utc e recusa DateTime com Kind local ou não especificado

Em todos, o valor vai como parâmetro. Montar a data na string do SQL arrisca injeção, conversões de tipo e, em alguns bancos, uma comparação de tipos diferentes que impede o uso do índice.

Encontrar as consultas que precisam de índice

Não adivinhe: o banco registra o que está lento.

-- Requer a extensão pg_stat_statements
SELECT query, calls, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY mean_exec_time * calls DESC
LIMIT 10;

A ordenação por tempo médio × número de chamadas encontra as consultas que mais pesam no total: uma consulta de 5 ms chamada um milhão de vezes por dia pesa mais que uma de 2 segundos chamada uma vez.

-- Índices que nunca foram usados desde a última reinicialização das estatísticas
SELECT relname AS tabela, indexrelname AS indice, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

Lista de revisão

Ao escrever uma consulta

  • Existe um índice que atende o WHERE e o ORDER BY desta consulta?
  • A coluna do filtro aparece sem função e sem conversão de tipo?
  • A busca de texto evita curinga no início?
  • A paginação usa cursor, e não OFFSET grande?

Ao criar um índice

  • As colunas de igualdade vêm antes das de intervalo e ordenação?
  • Ele não repete o começo de outro índice existente?
  • Um índice parcial atenderia com menos espaço?
  • Em produção, ele é criado com CONCURRENTLY ou equivalente?

Periodicamente

  • As consultas mais pesadas em pg_stat_statements usam índices?
  • Há índices com zero leituras que podem ser removidos?
  • As chaves estrangeiras usadas em junções têm índice?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Ler planos de execução na prática

    Quero aprender a ler planos de execução com EXPLAIN ANALYZE.
    
    Pergunte qual banco eu uso e, se possível, me ajude a montar uma tabela de teste com alguns milhões de linhas geradas, com o script SQL completo.
    
    Depois, vamos fazer juntos, uma consulta por vez:
    1. Uma busca por igualdade sem índice. Peça que eu rode o EXPLAIN ANALYZE e cole o resultado. Explique cada linha do plano.
    2. A mesma busca depois de criar o índice. Compare os planos.
    3. Uma consulta com ORDER BY e LIMIT, antes e depois de um índice composto na ordem certa.
    4. Uma consulta com lower() sobre a coluna, antes e depois de um índice sobre expressão.
    5. Uma consulta de baixa seletividade, para ver o planejador escolhendo a varredura.
    
    Em cada plano, me peça para apontar o problema antes de você explicar.
  • Criar índices em produção sem parar o sistema

    Preciso criar um índice numa tabela grande em produção sem bloquear as gravações.
    
    Pergunte qual banco e qual ferramenta de migração eu uso.
    
    Explique:
    1. Por que o CREATE INDEX comum bloqueia a tabela e por quanto tempo, aproximadamente.
    2. Como criar sem bloquear no meu banco (por exemplo, CREATE INDEX CONCURRENTLY no PostgreSQL), o que essa forma custa e o que acontece se ela falhar no meio.
    3. Como configurar a minha ferramenta de migração para rodar esse comando fora de uma transação.
    4. Como acompanhar o progresso da criação.
    5. Como conferir depois que o índice ficou válido e está sendo usado.
    6. Como remover um índice antigo com o mesmo cuidado.
    
    Termine com um roteiro passo a passo que eu possa seguir na próxima vez.
  • Revisar consultas e índices

    Vou colar abaixo uma ou mais consultas SQL do meu sistema, os índices existentes nas tabelas envolvidas e, se tiver, o resultado do EXPLAIN ANALYZE. Revise com foco no uso de índices.
    
    Para cada consulta, verifique:
    1. Existe um índice que atende o WHERE, o JOIN e o ORDER BY? Com as colunas na ordem certa?
    2. Alguma coluna do filtro aparece dentro de função, com conversão de tipo ou com curinga no início?
    3. A paginação usa OFFSET grande?
    4. O plano mostra varredura sequencial, ordenação desnecessária ou muitas linhas descartadas pelo filtro?
    
    Depois revise os índices existentes:
    5. Há índices redundantes, que são o começo de outro?
    6. Há índices que provavelmente não são usados?
    7. As chaves estrangeiras usadas em junções têm índice?
    
    Para cada problema, explique o impacto e mostre a consulta ou o índice corrigido. Não comente o que já está correto.
    
    Consultas, índices e planos:
    [cole aqui]