ActiveRecord: risolvere i problemi di query N+1 in Ruby on Rails
Guida completa per individuare e risolvere le query N+1 in Rails con ActiveRecord. Padroneggia includes, preload, eager_load e gli strumenti di rilevamento automatico.

Le query N+1 rappresentano uno dei problemi di prestazioni più comuni nelle applicazioni Rails. Un semplice ciclo sui record può scatenare centinaia di query SQL inutili, rallentando drasticamente i tempi di risposta. Questa guida copre le tecniche di rilevamento e risoluzione per garantire applicazioni Rails performanti.
Una pagina che mostra 50 articoli con i relativi autori può generare 51 query SQL invece di una sola. In produzione con migliaia di utenti, questo problema diventa critico per i tempi di risposta e il carico del server.
Comprendere il problema N+1
Il problema N+1 si verifica quando il codice esegue una query per recuperare un elenco di record (1 query), poi esegue una query aggiuntiva per ciascun record per accedere alle sue associazioni (N query). Il nome "N+1" descrive esattamente questo schema: 1 query iniziale + N query per le associazioni.
Un esempio concreto con articoli e relativi autori illustra il problema. Senza ottimizzazione, ogni accesso all'autore di un articolo scatena una nuova query SQL.
# 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
endNella view, ogni chiamata a article.author scatena una query aggiuntiva al database.
<!-- app/views/articles/index.html.erb -->
<!-- This view generates N additional queries -->
<% @articles.each do |article| %>
<div class="article">
<h2><%= article.title %></h2>
<!-- Each call generates: SELECT * FROM users WHERE id = ? -->
<p>By <%= article.author.name %></p>
</div>
<% end %>Per 100 articoli, questo codice genera 101 query SQL. I log di Rails mostrano chiaramente il problema con query ripetitive.
-- 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 queriesRisolvere con includes
Il metodo includes è la soluzione più comune e raccomandata per correggere i problemi N+1. Indica ad ActiveRecord di precaricare le associazioni in una o due query ottimizzate.
# 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
endCon includes, ActiveRecord esegue solo due query indipendentemente dal numero di articoli. La prima recupera tutti gli articoli, la seconda recupera tutti gli autori pertinenti.
-- Rails logs with includes (only 2 queries)
SELECT "articles".* FROM "articles"
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...)Le associazioni annidate possono essere precaricate utilizzando la sintassi a hash. Questo approccio è essenziale quando le view accedono a più livelli di associazioni.
# 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
endSe una view accede a un'associazione all'interno di un ciclo, tale associazione deve essere precaricata nel controller con includes. Conviene verificare sempre i pattern di accesso alle associazioni nelle view.
Differenze tra includes, preload ed eager_load
Rails offre tre metodi per il precaricamento delle associazioni. Ciascuno utilizza una strategia SQL differente, con casi d'uso specifici.
preload: query separate
Il metodo preload esegue sempre query separate per ciascuna associazione. Funziona in modo efficiente quando nessuna condizione WHERE filtra sulle associazioni.
# 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
Il metodo eager_load utilizza un LEFT OUTER JOIN per caricare i dati in un'unica query. Diventa obbligatorio quando si filtra o si ordina su colonne delle associazioni.
# 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" ASCincludes: comportamento intelligente
Il metodo includes sceglie automaticamente la strategia migliore. Utilizza preload per impostazione predefinita, ma passa a eager_load se una clausola WHERE fa riferimento all'associazione.
# 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
endLa tabella seguente riassume le differenze tra i tre metodi.
| Metodo | Strategia SQL | Caso d'uso |
|--------|---------------|------------|
| preload | Query separate | Precaricamento semplice, senza filtraggio |
| eager_load | LEFT OUTER JOIN | Filtraggio/ordinamento su associazioni |
| includes | Automatico | Uso generale, valore predefinito raccomandato |
Rilevamento automatizzato di N+1
Il rilevamento manuale dei problemi N+1 è tedioso e soggetto a errori. Diversi strumenti automatizzano questo rilevamento in sviluppo e CI.
Bullet: rilevamento in tempo reale
La gem Bullet analizza le query SQL in tempo reale e segnala i problemi N+1 rilevati. Suggerisce inoltre le correzioni appropriate.
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
endLa configurazione nell'ambiente di sviluppo permette di attivare diverse modalità di avviso.
# 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
endQuando viene rilevato un problema N+1, Bullet visualizza un messaggio esplicito con la soluzione raccomandata.
# 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:5In integrazione continua, abilitare Bullet.raise = true fa fallire i test se viene rilevato un problema N+1. Questo previene le regressioni di prestazioni.
Prosopite: alternativa leggera
La gem Prosopite offre un'alternativa più leggera a Bullet, con configurazione minima e compatibilità con i test.
# Gemfile
# Prosopite as an alternative to Bullet
group :development, :test do
gem 'prosopite'
end# config/environments/development.rb
# Prosopite configuration
Rails.application.configure do
config.after_initialize do
Prosopite.rails_logger = true
Prosopite.raise = Rails.env.test?
end
endTecniche avanzate di ottimizzazione
Oltre ai metodi di base, diverse tecniche permettono di affinare l'ottimizzazione delle query ActiveRecord.
Strict Loading: prevenzione predefinita
Rails 6.1+ fornisce una modalità di caricamento rigoroso che solleva un'eccezione se viene effettuato l'accesso a un'associazione non precaricata. Questo approccio preventivo costringe a risolvere gli N+1 durante lo sviluppo.
# 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
endIl caricamento rigoroso può essere abilitato anche per una specifica query.
# 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
endSelect e Pluck per dati parziali
Quando servono solo alcune colonne, select e pluck riducono la quantità di dati trasferiti dal database.
# 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
endCounter Cache per i conteggi
I conteggi di associazioni (article.comments.count) generano una query SQL a ogni chiamata. Counter cache memorizza questo conteggio direttamente nella tabella padre.
# 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
endLa migrazione aggiunge la colonna del conteggio con un valore predefinito.
# 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
endDopo questa configurazione, article.comments_count legge direttamente la colonna senza query SQL aggiuntive.
# app/views/articles/index.html.erb
# Using counter cache (no SQL query)
<% @articles.each do |article| %>
<p><%= article.title %> - <%= article.comments_count %> comments</p>
<% end %>Pronto a superare i tuoi colloqui su Ruby on Rails?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Best practice e checklist
Un approccio sistematico previene i problemi N+1 nei nuovi sviluppi e corregge progressivamente il codice esistente.
Analisi della view prima del codice
Prima di scrivere il codice del controller, conviene analizzare la view per identificare tutte le associazioni utilizzate. Questa anticipazione evita dimenticanze.
# 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
endScope riutilizzabili
Centralizzare gli includes frequenti negli scope semplifica la manutenzione e garantisce coerenza.
# 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
}
endChecklist di prevenzione
Questa checklist riassume i punti essenziali di verifica per evitare i problemi N+1:
- Installare e configurare Bullet o Prosopite in sviluppo
- Abilitare Bullet.raise in CI per bloccare le regressioni
- Analizzare le view per identificare le associazioni prima di scrivere i controller
- Utilizzare
includesper impostazione predefinita,eager_loadse si filtra sulle associazioni - Creare scope riutilizzabili per i pattern di precaricamento frequenti
- Adottare strict loading sui modelli sensibili
- Aggiungere counter cache per i conteggi frequenti
- Verificare regolarmente i log SQL in sviluppo
Precaricare troppe associazioni consuma memoria inutilmente. Solo le associazioni effettivamente utilizzate dalla view dovrebbero essere precaricate. Strumenti come Bullet rilevano anche l'"unused eager loading".
Conclusione
Le query N+1 rappresentano un problema di prestazioni rilevante, facilmente prevenibile nelle applicazioni Rails. La combinazione di strumenti di rilevamento automatizzati e buone pratiche di sviluppo elimina questo problema in modo efficace.
Punti chiave:
includesrisolve la maggior parte dei casi N+1 precaricando le associazionieager_loadè necessario quando si filtra o si ordina sulle associazioni- Bullet e Prosopite rilevano automaticamente i problemi in sviluppo
- Strict loading previene gli N+1 sollevando eccezioni
- I counter cache ottimizzano i conteggi frequenti
- Analizzare le view prima di scrivere i controller evita dimenticanze
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Domande colloquio Ruby on Rails: Top 25 nel 2026
Le 25 domande più frequenti per i colloqui Ruby on Rails. Architettura MVC, Active Record, migration, testing RSpec, API REST con risposte dettagliate ed esempi di codice.

Ruby on Rails 7: Hotwire e Turbo per Applicazioni Reattive
Guida completa a Hotwire e Turbo in Rails 7. Costruire applicazioni reattive senza scrivere JavaScript con Turbo Drive, Frames e Streams.

Rails API Mode nel 2026: API RESTful, Serializzazione e Domande da Colloquio
Guida completa alla modalità API-only di Rails 8.1 nel 2026. Configurazione, serializzazione con Alba e jsonapi-serializer, autenticazione JWT, gestione errori strutturata e domande frequenti nei colloqui tecnici Ruby on Rails.