DevGuideTK
← Trilha Backend

Plano de execução

Como ler o EXPLAIN para entender por que uma consulta está lenta.

O código desta página foi executado nas cinco linguagens contra um PostgreSQL 18 com 200 mil clientes e 2 milhões de pedidos. Os planos e tempos do nível Conceito vêm do mesmo banco.

Um teste que falha quando a consulta não usa índice

A função recebe uma consulta e devolve as tabelas que ela varre por inteiro. Ela desliga a varredura completa só dentro de uma transação (SET LOCAL), pede o plano com EXPLAIN, sem executar a consulta, e procura Seq Scan no texto. Com a varredura desligada, o Seq Scan só sobra quando nenhum índice serve. Nos testes da aplicação, cada consulta crítica passa pela função, e o teste falha se a lista não vier vazia.

import pg from "pg";

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

/** Tabelas que a consulta varre por inteiro mesmo quando o banco é proibido de fazer isso. */
export async function tabelasSemIndice(sql: string): Promise<string[]> {
  const conexao = await pool.connect();
  try {
    await conexao.query("BEGIN");
    // Com a varredura completa desligada, ela só aparece se nenhum índice serve
    await conexao.query("SET LOCAL enable_seqscan = off");
    const { rows } = await conexao.query(`EXPLAIN ${sql}`); // só planeja, não executa
    const plano = rows.map((r) => r["QUERY PLAN"]).join("\n");
    return [...plano.matchAll(/Seq Scan on (\w+)/g)].map((m) => m[1]);
  } finally {
    await conexao.query("ROLLBACK");
    conexao.release();
  }
}

// Num teste: expect(await tabelasSemIndice(consulta)).toEqual([]);

O resultado, nas cinco linguagens

As consultas testadas:

SELECT * FROM pedidos WHERE cliente_id = 4242
SELECT * FROM clientes WHERE lower(email) = 'ana@exemplo.com'
SELECT * FROM pedidos WHERE cliente_id = 4242::numeric
SELECT p.* FROM pedidos p JOIN clientes c ON c.id = p.cliente_id WHERE c.email = 'ana@exemplo.com'
ConsultaEsperadoNas cinco
Pedidos por cliente_idnenhuma tabelanenhuma tabela
Clientes por lower(email), sem índice na expressãoclientesclientes
Pedidos por cliente_id comparado com numericpedidospedidos
Pedidos de um cliente achado pelo e-mail, com junçãonenhuma tabelanenhuma tabela

O teste pegou os dois erros que o nível Conceito mediu: a função na coluna e o tipo trocado. Numa tabela vazia com índice, o plano padrão mostrou Seq Scan, e com a varredura desligada mostrou Index Scan. Sem o SET LOCAL enable_seqscan = off, o teste daria alarme falso em todo banco de teste pequeno.

O que muda de uma linguagem para outra

Transação desfeita no fimLer o plano
TypeScriptBEGIN e ROLLBACK no finallyColuna QUERY PLAN de cada linha
Pythontransaction(force_rollback=True)Primeira coluna de cada linha
Godefer tx.Rollback(ctx)pgx.CollectRows com pgx.RowTo[string]
JavasetAutoCommit(false) e rollback() no finallygetString(1)
C#Transação descartada pelo await using, sem CommitGetString(0)

O SET LOCAL vale só até o fim da transação. Um SET comum ficaria na conexão e voltaria para o pool com a varredura desligada, afetando outras consultas, a mesma armadilha do tópico de autorização.

A função usa o texto da consulta com os valores já escritos. Consultas com parâmetros ($1) podem ser planejadas com o EXPLAIN e os parâmetros, mas cada driver trata isso de um jeito; para o teste, valores fixos bastam.

Comandos de investigação

Os planos, com tempos e páginas lidas:

EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

-- Para UPDATE e DELETE, sem aplicar a alteração
BEGIN;
EXPLAIN (ANALYZE, BUFFERS) DELETE FROM pedidos WHERE ...;
ROLLBACK;

As estimativas que o planejador usa para uma coluna:

SELECT n_distinct, most_common_vals, most_common_freqs
  FROM pg_stats
 WHERE tablename = 'eventos' AND attname = 'tipo';

As consultas que mais pesam, com a extensão pg_stat_statements habilitada:

SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time, 2) AS media_ms, query
  FROM pg_stat_statements
 ORDER BY total_exec_time DESC
 LIMIT 10;

O plano das consultas lentas no log, numa sessão (para todo o servidor, use session_preload_libraries ou shared_preload_libraries):

LOAD 'auto_explain';
SET auto_explain.log_min_duration = '50ms';
SET auto_explain.log_analyze = on;

As correções medidas no nível Conceito:

CREATE INDEX ON clientes (lower(email));               -- função na coluna
CREATE INDEX ON clientes (email text_pattern_ops);     -- LIKE 'prefixo%' com collation diferente de C
ANALYZE eventos;                                       -- estatísticas depois de uma carga

Os comandos com auto_explain, text_pattern_ops e ANALYZE foram executados no teste. As consultas em pg_stats e em pg_stat_statements não foram executadas; a segunda exige a extensão carregada em shared_preload_libraries, que o container de teste não tinha.

Lista de revisão

Consultas

  • As colunas do WHERE aparecem sem função, ou há índice na expressão?
  • Os parâmetros têm o mesmo tipo das colunas?
  • As listagens com ORDER BY e LIMIT têm índice na ordem?
  • Buscas por prefixo com LIKE têm índice compatível com a collation?

Investigação

  • O plano foi conferido com EXPLAIN (ANALYZE, BUFFERS) e volume realista?
  • As estimativas de linhas estão perto das reais?
  • O ANALYZE roda depois de cargas grandes?

Acompanhamento

  • O pg_stat_statements, ou o equivalente, está habilitado e alguém olha as consultas de maior tempo total?
  • O auto_explain ou o log de consultas lentas registra os planos das consultas acima de um limite?
  • As consultas críticas têm um teste que falha se deixarem de usar í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.

  • Testar automaticamente o uso de índices

    Quero um teste automatizado que falhe quando uma consulta crítica deixar de usar índice.
    
    Pergunte qual linguagem, framework de testes, driver e banco eu uso, e como o banco sobe nos testes.
    
    Vamos em etapas:
    1. A função que planeja a consulta com a varredura completa desligada só na transação.
    2. A extração das tabelas varridas por inteiro.
    3. Os testes das consultas críticas do meu sistema.
    4. Por que o teste precisa desligar a varredura completa num banco de teste pequeno.
    5. Como capturar as consultas geradas pelo ORM para testá-las.
  • Corrigir uma consulta com o plano

    Vou corrigir uma consulta lenta e quero fazer isso com segurança.
    
    Pergunte qual banco eu uso e peça a consulta e o plano atual.
    
    Me ajude a:
    1. Propor a correção (índice, reescrita, tipo, estatística), explicando a causa no plano.
    2. Escrever a migração do índice sem travar a tabela (CREATE INDEX CONCURRENTLY).
    3. Comparar os planos antes e depois, com volume realista.
    4. Conferir que a correção não piorou outras consultas.
    5. Acrescentar um teste que proteja a consulta.
  • Revisar consultas e índices

    Vou colar abaixo consultas SQL de uma aplicação (ou o código do ORM que as gera) e o esquema das tabelas com os índices. Revise o desempenho.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Há funções nas colunas do WHERE sem índice na expressão?
    2. Os tipos dos parâmetros batem com os das colunas?
    3. As listagens com ORDER BY e LIMIT têm índice na ordem?
    4. Há LIKE com curinga no começo usado como busca?
    5. Os índices compostos têm as colunas na ordem que as consultas usam?
    6. Há índices que nenhuma consulta usa?
    
    Para cada problema, diga como ele aparece no EXPLAIN e mostre a correção.
    
    Consultas e esquema:
    [cole aqui]