# ActiveRecord: Resolvendo problemas de consultas N+1 em Ruby on Rails > Guia completo para detectar e corrigir consultas N+1 em Rails com ActiveRecord. Domine includes, preload, eager_load e ferramentas de detecção automática. - Published: 2026-02-23 - Updated: 2026-04-29 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, activerecord, performance, n+1 queries, sql optimization - Reading time: 10 min --- As consultas N+1 representam um dos problemas de desempenho mais comuns em aplicações Rails. Um simples loop sobre registros pode disparar centenas de consultas SQL desnecessárias, retardando drasticamente os tempos de resposta. Este guia aborda as técnicas de detecção e resolução para garantir aplicações Rails performáticas. > **Impacto em produção** > > Uma página exibindo 50 artigos com seus autores pode gerar 51 consultas SQL em vez de apenas uma. Em produção com milhares de usuários, esse problema se torna crítico para os tempos de resposta e a carga do servidor. ## Compreendendo o problema N+1 O problema N+1 ocorre quando o código executa uma consulta para recuperar uma lista de registros (1 consulta), e em seguida executa uma consulta adicional para cada registro a fim de acessar suas associações (N consultas). O nome "N+1" descreve exatamente esse padrão: 1 consulta inicial + N consultas para as associações. Um exemplo concreto com artigos e seus autores ilustra o problema. Sem otimização, cada acesso ao autor de um artigo dispara uma nova consulta SQL. ```ruby # app/controllers/articles_controller.rb # Example code generating an N+1 problem class ArticlesController < ApplicationController def index # 1 query: SELECT * FROM articles @articles = Article.all end end ``` Na view, cada chamada a `article.author` dispara uma consulta adicional ao banco de dados. ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` Para 100 artigos, esse código gera 101 consultas SQL. Os logs do Rails mostram claramente o problema com consultas repetitivas. ```sql -- Rails logs showing the N+1 problem -- 1 initial query SELECT "articles".* FROM "articles" -- N queries for authors (repeated for each article) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1 SELECT "users".* FROM "users" WHERE "users"."id" = 3 LIMIT 1 -- ... 97 more queries ``` ## Resolvendo com includes O método `includes` é a solução mais comum e recomendada para corrigir problemas N+1. Ele instrui o ActiveRecord a pré-carregar as associações em uma ou duas consultas otimizadas. ```ruby # app/controllers/articles_controller.rb # Solution with includes - preloading authors class ArticlesController < ApplicationController def index # Preloads authors with articles # Generates only 2 queries instead of N+1 @articles = Article.includes(:author).all end end ``` Com `includes`, o ActiveRecord executa apenas duas consultas, independentemente do número de artigos. A primeira recupera todos os artigos, a segunda recupera todos os autores relevantes. ```sql -- Rails logs with includes (only 2 queries) SELECT "articles".* FROM "articles" SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...) ``` Associações aninhadas também podem ser pré-carregadas usando a sintaxe de hash. Essa abordagem é essencial quando as views acessam vários níveis de associações. ```ruby # app/controllers/articles_controller.rb # Preloading nested associations class ArticlesController < ApplicationController def index # Preloads author -> company and all comments @articles = Article.includes(author: :company, comments: :user) end end ``` > **Regra de ouro** > > Se uma view acessa uma associação dentro de um loop, essa associação deve ser pré-carregada no controller com `includes`. Verifique sempre os padrões de acesso a associações nas views. ## Diferenças entre includes, preload e eager_load O Rails fornece três métodos para pré-carregamento de associações. Cada um utiliza uma estratégia SQL diferente, com casos de uso específicos. ### preload: consultas separadas O método `preload` sempre executa consultas separadas para cada associação. Funciona com eficiência quando nenhuma condição WHERE filtra sobre as associações. ```ruby # app/models/article.rb # preload always uses separate queries class Article < ApplicationRecord scope :with_authors, -> { preload(:author) } end # Usage in controller @articles = Article.with_authors.limit(20) # SQL generated: # SELECT "articles".* FROM "articles" LIMIT 20 # SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, ...) ``` ### eager_load: LEFT OUTER JOIN O método `eager_load` utiliza um LEFT OUTER JOIN para carregar os dados em uma única consulta. Torna-se obrigatório ao filtrar ou ordenar por colunas de associações. ```ruby # app/controllers/articles_controller.rb # eager_load allows filtering on associations class ArticlesController < ApplicationController def verified_authors # Filters articles by author status # Requires eager_load because WHERE references users @articles = Article.eager_load(:author) .where(users: { verified: true }) .order("users.name ASC") end end # SQL generated (single query with JOIN): # SELECT "articles"."id", "articles"."title", ... # FROM "articles" # LEFT OUTER JOIN "users" ON "users"."id" = "articles"."author_id" # WHERE "users"."verified" = TRUE # ORDER BY "users"."name" ASC ``` ### includes: comportamento inteligente O método `includes` escolhe automaticamente a melhor estratégia. Usa `preload` por padrão, mas alterna para `eager_load` se uma cláusula WHERE referenciar a associação. ```ruby # app/controllers/articles_controller.rb # includes adapts automatically to context class ArticlesController < ApplicationController def index # No condition on association: uses preload (2 queries) @articles = Article.includes(:author).all end def by_verified_authors # With condition on association: uses eager_load (JOIN) @articles = Article.includes(:author) .where(users: { verified: true }) end end ``` A tabela a seguir resume as diferenças entre os três métodos. | Método | Estratégia SQL | Caso de uso | |--------|----------------|-------------| | `preload` | Consultas separadas | Pré-carregamento simples, sem filtragem | | `eager_load` | LEFT OUTER JOIN | Filtragem/ordenação em associações | | `includes` | Automático | Uso geral, padrão recomendado | ## Detecção automatizada de N+1 A detecção manual de problemas N+1 é tediosa e propensa a erros. Várias ferramentas automatizam essa detecção em desenvolvimento e CI. ### Bullet: detecção em tempo real A gem Bullet analisa as consultas SQL em tempo real e alerta sobre problemas N+1 detectados. Ela também sugere as correções apropriadas. ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` A configuração no ambiente de desenvolvimento permite ativar diversos modos de alerta. ```ruby # config/environments/development.rb # Bullet configuration to detect N+1 Rails.application.configure do config.after_initialize do Bullet.enable = true # Display JavaScript alert in browser Bullet.alert = true # Add footer with details Bullet.bullet_logger = true # Display in Rails logs Bullet.rails_logger = true # Raise exception (useful in CI) Bullet.raise = false end end ``` Quando um problema N+1 é detectado, o Bullet exibe uma mensagem explícita com a solução recomendada. ```text # Example Bullet alert in logs USE eager loading detected Article => [:author] Add to your query: .includes([:author]) Call stack: /app/views/articles/index.html.erb:5 ``` > **Bullet em CI** > > Na integração contínua, ativar `Bullet.raise = true` faz com que os testes falhem se um problema N+1 for detectado. Isso evita regressões de desempenho. ### Prosopite: alternativa leve A gem Prosopite oferece uma alternativa mais leve ao Bullet, com configuração mínima e compatibilidade com testes. ```ruby # Gemfile # Prosopite as an alternative to Bullet group :development, :test do gem 'prosopite' end ``` ```ruby # config/environments/development.rb # Prosopite configuration Rails.application.configure do config.after_initialize do Prosopite.rails_logger = true Prosopite.raise = Rails.env.test? end end ``` ## Técnicas avançadas de otimização Além dos métodos básicos, várias técnicas permitem ajustar com precisão a otimização das consultas ActiveRecord. ### Strict Loading: prevenção por padrão O Rails 6.1+ fornece um modo de carregamento estrito que lança uma exceção se uma associação não pré-carregada for acessada. Essa abordagem preventiva força a resolução dos N+1 durante o desenvolvimento. ```ruby # app/models/article.rb # Enable strict loading by default on the model class Article < ApplicationRecord # Any non-preloaded association access raises an exception self.strict_loading_by_default = true belongs_to :author has_many :comments end ``` O carregamento estrito também pode ser ativado para uma consulta específica. ```ruby # app/controllers/articles_controller.rb # Strict loading on a specific query class ArticlesController < ApplicationController def index # Raises StrictLoadingViolationError if a non-included # association is accessed @articles = Article.strict_loading.includes(:author) end end ``` ### Select e Pluck para dados parciais Quando apenas algumas colunas são necessárias, `select` e `pluck` reduzem a quantidade de dados transferidos do banco de dados. ```ruby # app/controllers/reports_controller.rb # Optimization with select and pluck class ReportsController < ApplicationController def titles_only # select returns Article objects with only id and title @articles = Article.select(:id, :title) end def title_array # pluck returns an Array of values, not AR objects # More performant when only values are needed @titles = Article.pluck(:title) # => ["First article", "Second article", ...] end end ``` ### Counter Cache para contagens As contagens de associações (`article.comments.count`) geram uma consulta SQL a cada chamada. O counter cache armazena essa contagem diretamente na tabela pai. ```ruby # app/models/comment.rb # Counter cache configuration class Comment < ApplicationRecord # Rails automatically maintains the counter in articles.comments_count belongs_to :article, counter_cache: true end ``` A migração adiciona a coluna de contagem com um valor padrão. ```ruby # db/migrate/20260223_add_comments_count_to_articles.rb # Migration to add counter cache class AddCommentsCountToArticles < ActiveRecord::Migration[7.1] def change add_column :articles, :comments_count, :integer, default: 0, null: false # Initialize counters for existing data Article.find_each do |article| Article.reset_counters(article.id, :comments) end end end ``` Após essa configuração, `article.comments_count` lê a coluna diretamente, sem consultas SQL adicionais. ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

<%= article.title %> - <%= article.comments_count %> comments

<% end %> ``` ## Boas práticas e checklist Uma abordagem sistemática previne os problemas N+1 em novos desenvolvimentos e corrige progressivamente o código existente. ### Análise da view antes do código Antes de escrever o código do controller, é recomendável analisar a view para identificar todas as associações acessadas. Essa antecipação evita esquecimentos. ```ruby # app/controllers/articles_controller.rb # Pre-analyze view to identify required includes class ArticlesController < ApplicationController def show # View accesses: author, author.company, comments, comments.user # All these must be preloaded @article = Article.includes( author: :company, comments: :user ).find(params[:id]) end end ``` ### Scopes reutilizáveis Centralizar os includes frequentes em scopes simplifica a manutenção e garante consistência. ```ruby # app/models/article.rb # Reusable scopes for preloading class Article < ApplicationRecord # Scope for list display scope :with_author, -> { includes(:author) } # Scope for detailed display scope :with_full_details, -> { includes( author: :company, comments: { user: :avatar_attachment }, tags: [] ) } # Scope for admin with all relations scope :for_admin, -> { includes(:author, :comments, :tags, :category) .with_attached_cover_image } end ``` ### Checklist de prevenção Esta checklist resume os pontos essenciais de verificação para evitar problemas N+1: - Instalar e configurar Bullet ou Prosopite em desenvolvimento - Ativar Bullet.raise em CI para bloquear regressões - Analisar as views para identificar associações antes de escrever os controllers - Usar `includes` por padrão, `eager_load` se houver filtragem em associações - Criar scopes reutilizáveis para os padrões de pré-carregamento frequentes - Utilizar strict loading em modelos sensíveis - Adicionar counter caches para contagens frequentes - Verificar regularmente os logs SQL em desenvolvimento > **Atenção ao excesso de pré-carregamento** > > Pré-carregar associações em excesso consome memória desnecessariamente. Apenas as associações realmente utilizadas pela view devem ser pré-carregadas. Ferramentas como Bullet também detectam o "unused eager loading". ## Conclusão As consultas N+1 representam um problema de desempenho importante, facilmente prevenível em aplicações Rails. A combinação de ferramentas de detecção automatizadas e boas práticas de desenvolvimento elimina esse problema com eficácia. **Pontos-chave:** - `includes` resolve a maioria dos casos N+1 ao pré-carregar as associações - `eager_load` é necessário ao filtrar ou ordenar por associações - Bullet e Prosopite detectam automaticamente os problemas em desenvolvimento - Strict loading previne os N+1 lançando exceções - Counter caches otimizam as contagens frequentes - Analisar as views antes de escrever os controllers evita esquecimentos --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems