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=1000eactual time=1totaliza 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.