---
title: "A pressa de culpar o ActiveRecord antes de abrir o plano de execução no PostgreSQL"
author: "Pedro Dalben"
date: "2026-10-18T11:03:40Z"
category: "Ruby on Rails"
reading_time: "7 min"
canonical: "https://pedrodalben.com.br/blog/a-pressa-de-culpar-o-activerecord-antes-de-abrir-o-plano-de-execucao-no-postgresql"
---

# A pressa de culpar o ActiveRecord antes de abrir o plano de execução no PostgreSQL

> Antes de espalhar includes pelo código ou criar índices redundantes, entender o EXPLAIN ANALYZE mostra onde o banco realmente perde tempo.

**Autor:** Pedro Dalben (CTO no Integrar Plus)  
**Publicado em:** 18 de October de 2026  
**URL Canônica Oficial:** [https://pedrodalben.com.br/blog/a-pressa-de-culpar-o-activerecord-antes-de-abrir-o-plano-de-execucao-no-postgresql](https://pedrodalben.com.br/blog/a-pressa-de-culpar-o-activerecord-antes-de-abrir-o-plano-de-execucao-no-postgresql)  

---


Quando uma requisição em Rails passa de dois segundos em produção, a equipe costuma se dividir em dois lados quase religiosos. O primeiro grupo aponta imediatamente o dedo para o ActiveRecord, assume que existe uma consulta N+1 escondida na view e sai distribuindo chamadas de `includes` sem critério até silenciar alertas de ferramentas locais. O segundo grupo prefere culpar a infraestrutura do banco de dados, criando migrations em série para adicionar índices compostos em qualquer coluna mencionada em cláusulas `where`.

Eu discordo das duas posturas. Ambas partem do mesmo vício, que é tentar resolver lentidão operando no escuro, guiando-se por sintomas superficiais em vez de evidências de execução. Em servidores virtuais com recursos contidos de memória e disco, espalhar índices cegamente ou carregar árvores inteiras de associações na memória do Ruby costuma agravar o problema em vez de trazer alívio.

## A miragem do N+1 como culpado universal

A primeira postura parte da premissa de que qualquer consulta repetida representa uma falha grave de arquitetura. Não representa. Quando operamos Rails sobre PostgreSQL na mesma máquina ou em uma rede local com latência de frações de milissegundo via socket Unix, trinta consultas que buscam registros individuais pela chave primária demoram, somadas, menos de três milissegundos no banco. Se as páginas de dados já residem na memória do PostgreSQL, o impacto dessas requisições no processador do banco é quase imperceptível.

O perigo real surge na solução apressada. Para eliminar o aviso de N+1 na listagem, o desenvolvedor troca essas consultas pequenas por um `eager_load` amplo com vários `LEFT OUTER JOIN`. O PostgreSQL resolve a junção com eficiência e devolve uma matriz relacional densa, cheia de colunas duplicadas. O problema é que o ActiveRecord precisa instanciar milhares de objetos Ruby correspondentes a essas linhas repetidas. O tempo de execução da consulta cai para vinte milissegundos, mas a requisição inteira trava por meio segundo porque o interpretador Ruby entrou em coleta de lixo pesada para alocar e desalocar memória. A tentativa ingênua de poupar o banco transfere uma carga desproporcional para a CPU da aplicação.

## O reflexo de criar índices sem critério

A segunda postura, comum em quem prefere ignorar o código da aplicação, trata índices como remédio sem efeitos colaterais. O raciocínio costuma ser direto: se uma busca com filtro de status ou data está demorando, basta criar um índice nessa coluna. Essa hipótese desconsidera a forma como o planejador de consultas do PostgreSQL decide percorrer uma tabela.

Índices têm custo de manutenção contínuo. Toda operação de escrita, seja uma inserção ou uma atualização, precisa reorganizar a árvore do índice. Além disso, cada índice ocupa espaço no cache compartilhado do banco. Quando você cria índices redundantes, disputa páginas de memória RAM que deveriam abrigar registros acessados com frequência. Se o planejador estimar que a condição de filtro vai retornar uma fatia relevante das linhas da tabela, ele vai descartar o índice e executar uma varredura sequencial direta. Criar um índice que o planejador rejeita apenas desperdiça disco, atrasa os inserts e consome cache útil.

## Como dissecar o plano com buffers

A única forma técnica de diagnosticar o gargalo consiste em executar a consulta no console do Rails ou no cliente do banco precedida por `EXPLAIN (ANALYZE, BUFFERS)`. O comando `EXPLAIN` isolado apenas exibe a estimativa teórica do planejador, baseada nas estatísticas coletadas. O modificador `ANALYZE` executa a consulta de verdade e registra a duração real de cada etapa. Já o modificador `BUFFERS` expõe de onde os dados saíram, diferenciando memória de disco.

```sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM invoices
WHERE account_id = 42 AND status_code = 1;
```

A leitura desse resultado não exige decifrar cada detalhe matemático do planejador, mas requer atenção a três pontos determinantes:

A discrepância entre o custo estimado e as linhas reais revela o estado das estatísticas. O planejador exibe algo como `rows=12` na estimativa e, logo ao lado, `rows=15000` na execução real. Quando a previsão difere por ordens de grandeza da realidade, o banco está calculando planos com base em informações defasadas. Nenhum índice novo vai corrigir uma escolha ruim se o catálogo estatístico acreditar que a tabela contém dez registros quando na verdade abriga centenas de milhares. Nesses casos, rodar um comando `ANALYZE` na tabela resolve a lentidão sem demandar nenhuma alteração no código Ruby.

A contagem de buffers compartilhados separa problemas de memória de gargalos de armazenamento. A linha de buffers divide o acesso entre `shared hit` e `shared read`. Os blocos marcados como `hit` foram encontrados diretamente na memória RAM reservada pelo banco. Os blocos marcados como `read` precisaram ser buscados no armazenamento persistente do servidor. Se uma consulta simples consome quinhentos milissegundos e apresenta milhares de blocos em `read`, o problema reside em saturação de leitura no disco ou em cache subdimensionado, e nenhuma reescrita de escopo no ActiveRecord vai contornar esse limite físico.

A natureza do nó de varredura contextualiza a necessidade de índices. Um nó de `Seq Scan` em uma tabela com milhões de registros justifica intervenção imediata, mas o mesmo nó em uma tabela pequena de configurações é a opção mais econômica que o motor possui, já que ler o bloco inteiro sequencialmente custa menos do que alternar entre a árvore do índice e os dados da tabela. Da mesma forma, identificar um `Bitmap Heap Scan` seguido por um descarte massivo de linhas em `Rows Removed by Filter` aponta exatamente qual coluna complementar está faltando em um índice composto existente.

## Uma rotina prática de diagnóstico

Admito que ler planos com dezenas de junções e subconsultas pode parecer confuso no início, mas a rotina de triagem precisa ser metódica. Eu sigo um fluxo rígido antes de aprovar qualquer mudança de código ou migration em produção.

Primeiro, extraio a instrução SQL exata gerada pelo log do Rails e executo a análise com buffers diretamente no PostgreSQL. Se o banco conclui a operação em menos de dez milissegundos com leituras concentradas em `shared hit`, o banco está saudável. Nesse cenário, paro de procurar culpados no PostgreSQL e volto a atenção para a camada do Ruby: inspeciono o volume de objetos alocados, verifico se a serialização de dados não está pesada e avalio se parciais do ActionView não estão consumindo tempo excessivo de renderização.

Se a consulta no banco de fato concentrar o tempo de resposta, avalio os nós do plano. Só adiciono um índice se houver evidência clara de varredura sequencial em volume grande de dados ou descarte elevado após a leitura do índice atual. Caso o problema envolva N+1 genuíno, com centenas de viagens de rede acumulando latência real, prefiro usar `preload` para separar as buscas em duas etapas simples em vez de forçar um `eager_load` que incha a memória da aplicação com junções cartesianas.

O ActiveRecord tem particularidades conhecidas e o PostgreSQL obedece aos limites do hardware onde opera. Tratar problemas de desempenho como uma disputa de culpa entre o framework e o banco só produz soluções paliativas. Quem aprende a inspecionar os buffers e o custo real de execução para de criar índices inúteis e para de sacrificar a memória do servidor com carregamentos antecipados que a aplicação nunca solicitou.
