██╗██████╗ ███████╗██████╗ ██╗ █████╗ ███████╗
     ██║██╔══██╗╚══███╔╝██╔══██╗██║██╔══██╗██╔════╝
     ██║██████╔╝  ███╔╝ ██║  ██║██║███████║███████╗
██   ██║██╔═══╝  ███╔╝  ██║  ██║██║██╔══██║╚════██║
╚█████╔╝██║     ███████╗██████╔╝██║██║  ██║███████║
 ╚════╝ ╚═╝     ╚══════╝╚═════╝ ╚═╝╚═╝  ╚═╝╚══════╝   
⠀⠀
    

Índices: Quando a Query Parece Simples Mas o Banco Sofre

A query era ridiculamente simples. Listar notas fiscais de saída de um período, ordenadas por data. Funcionava instantâneo no meu Postgres local com umas duzentas linhas de seed.

Subi pro ambiente de staging com dump parcial de produção. Mesma query. Oito segundos. Eu fiquei olhando pro terminal achando que tinha colado errado.

Não tinha.

Rodei EXPLAIN ANALYZE:

EXPLAIN ANALYZE
SELECT *
FROM invoices
WHERE company_id = 42
  AND issued_at BETWEEN '2025-01-01' AND '2025-01-31'
ORDER BY issued_at DESC;

O plano voltou com Seq Scan on invoices. O Postgres varreu a tabela inteira, filtrou linha por linha, ordenou o resultado na memória. Com volume, isso escala linearmente pro pior lado.

Eu tinha índice em company_id. Só que o planner olhou pros números e decidiu que varrer tudo ainda era mais barato, ou o índice simples não ajudava no filtro de data + ordenação.

Criei:

CREATE INDEX idx_invoices_company_issued
ON invoices (company_id, issued_at DESC);

Mesma query. Index Scan. Tempo caiu pra menos de 50ms.

A ordem das colunas importa. company_id primeiro porque é igualdade. issued_at DESC depois porque cobre filtro de range e a ordenação que eu já pedia no ORDER BY.

Índice simples em cada coluna separada não resolveu porque o banco ainda precisava juntar informação de dois índices ou fazer sort extra.

No Active Record a query parece inofensiva:

Invoice.where(company_id: 42)
       .where(issued_at: period)
       .order(issued_at: :desc)

Funciona. Até não funcionar. O ORM não te avisa que falta índice. Só fica lento quando cresce.

O N+1 é o vilão famoso. Query lenta por plano ruim não é tão famoso assim. Descobri os dois no mesmo projeto fiscal.

Índice não é de graça. Cada insert/update precisa manter a árvore B-tree atualizada. Coloquei índice em coluna que filtrava busca rara e paguei custo de escrita em toda nota emitida por quase nenhum ganho de leitura.

Regra que adotei depois: medir com EXPLAIN antes de indexar no chute. E revisitar quando o padrão de acesso muda.

Antes de otimizar código Ruby, olho o plano. Metade das vezes o problema está três linhas abaixo do controller, no SQL que eu nem escrevi diretamente.

Fora isso… Obrgiado pela atenção, um abraço e até mais!

j