Por que queries lentas são um problema real

Uma query que demora 2 segundos pode parecer aceitável no desenvolvimento, mas em produção com 500 usuários simultâneos isso vira um gargalo que derruba o servidor. O banco de dados é frequentemente o ponto de estrangulamento mais ignorado em aplicações web — e também o que mais impacta a experiência do usuário.

Antes de jogar mais hardware no problema, vale entender exatamente o que está acontecendo.


EXPLAIN ANALYZE: lendo o plano de execução

O EXPLAIN ANALYZE executa a query e mostra o plano de execução com custos estimados e reais.

EXPLAIN ANALYZE
SELECT u.id, u.name, COUNT(o.id) AS total_orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2025-01-01'
GROUP BY u.id, u.name
ORDER BY total_orders DESC
LIMIT 20;

Output típico:

Limit  (cost=1234.56..1234.61 rows=20 width=48) (actual time=45.231..45.238 rows=20 loops=1)
  ->  Sort  (cost=1234.56..1259.56 rows=10000 width=48) (actual time=45.229..45.232 rows=20 loops=1)
        ->  HashAggregate  (cost=850.00..950.00 rows=10000 width=48) (actual time=41.100..43.500 rows=9500 loops=1)
              ->  Hash Left Join  (cost=250.00..700.00 rows=30000 width=16) (actual time=5.230..28.100 rows=30000 loops=1)
                    ->  Seq Scan on users  (cost=0.00..180.00 rows=5000 width=12) (actual time=0.010..3.200 rows=5000 loops=1)
                          Filter: (created_at > '2025-01-01')

O que prestar atenção:

  • cost=: estimativa do planejador. O primeiro número é custo de startup, o segundo é custo total.
  • actual time=: tempo real em milissegundos. Se estiver muito diferente do estimado, as estatísticas do banco podem estar desatualizadas — rode ANALYZE.
  • rows=: número de linhas processadas. Diferença grande entre estimado e real indica estatísticas ruins.
  • loops=: quantas vezes aquele nó foi executado. Um nó com loops=1000 e actual time=1 totaliza 1 segundo.

Seq Scan vs Index Scan

Seq Scan (varredura sequencial): o PostgreSQL lê a tabela inteira, linha por linha. Inevitável quando não há índice adequado ou quando a query retorna uma fração grande da tabela.

Index Scan: usa um índice para localizar as linhas. Muito mais rápido para buscas seletivas.

-- Sem índice: Seq Scan
SELECT * FROM orders WHERE user_id = 42;

-- Com índice: Index Scan
CREATE INDEX idx_orders_user_id ON orders(user_id);
SELECT * FROM orders WHERE user_id = 42;

O PostgreSQL escolhe Seq Scan quando estima que o índice não compensa — por exemplo, se a query retorna mais de 10-15% das linhas. Isso é correto. Não crie índices aleatoriamente esperando mágica.


pg_stat_statements: encontrando as queries mais lentas

pg_stat_statements é uma extensão que registra estatísticas de todas as queries executadas. É a forma mais eficiente de identificar gargalos em produção.

Habilitando:

-- No postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all

-- Depois de reiniciar o PostgreSQL
CREATE EXTENSION pg_stat_statements;

Consultando as queries mais lentas:

SELECT
  query,
  calls,
  round(total_exec_time::numeric, 2) AS total_ms,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  round(stddev_exec_time::numeric, 2) AS stddev_ms,
  rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Olhe tanto para mean_exec_time (queries individualmente lentas) quanto para total_exec_time (queries que acumulam tempo por serem chamadas com frequência). Uma query de 5ms chamada 100.000 vezes pode ser mais problemática que uma query de 2 segundos chamada raramente.


Dicas de indexação práticas

Índice composto: útil quando a query filtra por múltiplas colunas. A ordem importa — coloque as colunas mais seletivas primeiro, ou siga a ordem do WHERE.

-- Query
SELECT * FROM orders WHERE user_id = 42 AND status = 'pending';

-- Índice composto adequado
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

Partial index: índice sobre um subconjunto de linhas. Menor, mais rápido, ideal para valores específicos.

-- Só indexa orders pendentes (fração pequena da tabela)
CREATE INDEX idx_orders_pending ON orders(user_id)
WHERE status = 'pending';

Covering index: inclui todas as colunas que a query precisa, evitando acesso à tabela (Index Only Scan).

CREATE INDEX idx_orders_covering ON orders(user_id)
INCLUDE (status, total, created_at);

Exemplo prático em Ruby on Rails

O problema de N+1 é o mais comum em aplicações Rails: uma query que carrega N registros e depois dispara mais N queries para associações.

# N+1: 1 query para users + 1 query por user para orders
User.where(active: true).each do |user|
  puts user.orders.count
end
# Correto: eager loading com includes
User.where(active: true).includes(:orders).each do |user|
  puts user.orders.size  # não dispara nova query
end

Quando só precisa de alguns campos, use select ou pluck:

# Traz apenas o necessário
User.where(active: true).select(:id, :name, :email)

# Retorna array de valores, sem instanciar objetos ActiveRecord
User.where(active: true).pluck(:id, :name)

A partir do Rails 7.1, o método .explain aceita opções diretamente, sem precisar de gems extras:

# EXPLAIN simples
User.where(active: true).explain
# => EXPLAIN SELECT "users".* FROM "users" WHERE "users"."active" = true

# EXPLAIN ANALYZE com opções (Rails 7.1+)
User.where(active: true).explain(:analyze, :verbose, :buffers)
# => EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT ...

Isso mostra o plano de execução real com tempos, buffers e detalhes — direto no console do Rails, sem SQL manual.


Ferramentas úteis

pgBadger: analisa logs do PostgreSQL e gera relatório HTML detalhado com queries lentas, erros e estatísticas de uso. Ideal para análise pós-mortem.

pgbadger /var/log/postgresql/postgresql.log -o relatorio.html

Configure no postgresql.conf para logar queries lentas:

log_min_duration_statement = 500  # loga queries que demoram mais de 500ms
log_line_prefix = '%t [%p]: [%l-1] '

auto_explain: extensão que loga automaticamente o plano de execução de queries lentas, sem precisar rodar EXPLAIN manualmente.

# postgresql.conf
shared_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = 1000  # planos de queries > 1s
auto_explain.log_analyze = true

Debugar queries no PostgreSQL não é mágica — é processo. Com EXPLAIN ANALYZE, pg_stat_statements e boas práticas de indexação, você consegue identificar e resolver a maioria dos problemas de performance antes que eles virem incidentes em produção.


Referências