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([]);import os
import re
import psycopg
conexao = psycopg.connect(os.environ["DATABASE_URL"])
def tabelas_sem_indice(sql: str) -> list[str]:
"""Tabelas que a consulta varre por inteiro mesmo quando o banco é proibido de fazer isso."""
with conexao.transaction(force_rollback=True):
# Com a varredura completa desligada, ela só aparece se nenhum índice serve
conexao.execute("SET LOCAL enable_seqscan = off")
linhas = conexao.execute(f"EXPLAIN {sql}").fetchall() # só planeja, não executa
plano = "\n".join(linha[0] for linha in linhas)
return re.findall(r"Seq Scan on (\w+)", plano)
# Num teste: assert tabelas_sem_indice(consulta) == []package main
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var varreduraCompleta = regexp.MustCompile(`Seq Scan on (\w+)`)
// tabelasSemIndice devolve as tabelas que a consulta varre por inteiro mesmo quando
// o banco é proibido de fazer isso.
func tabelasSemIndice(ctx context.Context, db *pgxpool.Pool, sql string) ([]string, error) {
tx, err := db.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
// Com a varredura completa desligada, ela só aparece se nenhum índice serve
if _, err := tx.Exec(ctx, "SET LOCAL enable_seqscan = off"); err != nil {
return nil, err
}
linhas, err := tx.Query(ctx, "EXPLAIN "+sql) // só planeja, não executa
if err != nil {
return nil, err
}
plano, err := pgx.CollectRows(linhas, pgx.RowTo[string])
if err != nil {
return nil, err
}
tabelas := []string{}
for _, m := range varreduraCompleta.FindAllStringSubmatch(strings.Join(plano, "\n"), -1) {
tabelas = append(tabelas, m[1])
}
return tabelas, nil
}
// Num teste: if len(tabelas) > 0 { t.Errorf("sem índice: %v", tabelas) }import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class Plano {
static final Pattern VARREDURA_COMPLETA = Pattern.compile("Seq Scan on (\\w+)");
/** Tabelas que a consulta varre por inteiro mesmo quando o banco é proibido de fazer isso. */
static List<String> tabelasSemIndice(Connection db, String sql) throws SQLException {
db.setAutoCommit(false);
try (var comando = db.createStatement()) {
// Com a varredura completa desligada, ela só aparece se nenhum índice serve
comando.execute("SET LOCAL enable_seqscan = off");
var plano = new StringBuilder();
try (var linhas = comando.executeQuery("EXPLAIN " + sql)) { // só planeja, não executa
while (linhas.next()) plano.append(linhas.getString(1)).append('\n');
}
var tabelas = new ArrayList<String>();
var m = VARREDURA_COMPLETA.matcher(plano);
while (m.find()) tabelas.add(m.group(1));
return tabelas;
} finally {
db.rollback();
}
}
// Num teste: assertEquals(List.of(), tabelasSemIndice(db, consulta));
}using System.Text.RegularExpressions;
using Npgsql;
await using var db = NpgsqlDataSource.Create(Environment.GetEnvironmentVariable("DATABASE_URL")!);
// Tabelas que a consulta varre por inteiro mesmo quando o banco é proibido de fazer isso
async Task<List<string>> TabelasSemIndice(string sql)
{
await using var conexao = await db.OpenConnectionAsync();
await using var tx = await conexao.BeginTransactionAsync(); // desfeita ao descartar
// Com a varredura completa desligada, ela só aparece se nenhum índice serve
await using (var desligar = new NpgsqlCommand("SET LOCAL enable_seqscan = off", conexao, tx))
await desligar.ExecuteNonQueryAsync();
var plano = new List<string>();
await using var explicar = new NpgsqlCommand($"EXPLAIN {sql}", conexao, tx); // só planeja
await using var linhas = await explicar.ExecuteReaderAsync();
while (await linhas.ReadAsync()) plano.Add(linhas.GetString(0));
return Regex.Matches(string.Join('\n', plano), @"Seq Scan on (\w+)")
.Select(m => m.Groups[1].Value).ToList();
}
// Num teste: Assert.Empty(await TabelasSemIndice(consulta));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'| Consulta | Esperado | Nas cinco |
|---|---|---|
Pedidos por cliente_id | nenhuma tabela | nenhuma tabela |
Clientes por lower(email), sem índice na expressão | clientes | clientes |
Pedidos por cliente_id comparado com numeric | pedidos | pedidos |
| Pedidos de um cliente achado pelo e-mail, com junção | nenhuma tabela | nenhuma 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 fim | Ler o plano | |
|---|---|---|
| TypeScript | BEGIN e ROLLBACK no finally | Coluna QUERY PLAN de cada linha |
| Python | transaction(force_rollback=True) | Primeira coluna de cada linha |
| Go | defer tx.Rollback(ctx) | pgx.CollectRows com pgx.RowTo[string] |
| Java | setAutoCommit(false) e rollback() no finally | getString(1) |
| C# | Transação descartada pelo await using, sem Commit | GetString(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 cargaOs 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
WHEREaparecem sem função, ou há índice na expressão? - Os parâmetros têm o mesmo tipo das colunas?
- As listagens com
ORDER BYeLIMITtêm índice na ordem? - Buscas por prefixo com
LIKEtê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
ANALYZEroda 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_explainou 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]