# 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. - 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 --- 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. > **Impatto in produzione** > > 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. ```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 ``` Nella view, ogni chiamata a `article.author` scatena una query aggiuntiva al database. ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` Per 100 articoli, questo codice genera 101 query SQL. I log di Rails mostrano chiaramente il problema con query ripetitive. ```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 ``` ## Risolvere 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. ```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 ``` Con `includes`, ActiveRecord esegue solo due query indipendentemente dal numero di articoli. La prima recupera tutti gli articoli, la seconda recupera tutti gli autori pertinenti. ```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, ...) ``` Le associazioni annidate possono essere precaricate utilizzando la sintassi a hash. Questo approccio è essenziale quando le view accedono a più livelli di associazioni. ```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 ``` > **Regola d'oro** > > Se 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. ```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 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. ```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 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. ```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 ``` La 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. ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` La configurazione nell'ambiente di sviluppo permette di attivare diverse modalità di avviso. ```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 viene rilevato un problema N+1, Bullet visualizza un messaggio esplicito con la soluzione raccomandata. ```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 in CI** > > In 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. ```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 ``` ## Tecniche 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. ```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 ``` Il caricamento rigoroso può essere abilitato anche per una specifica query. ```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 per dati parziali Quando servono solo alcune colonne, `select` e `pluck` riducono la quantità di dati trasferiti dal database. ```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 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. ```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 ``` La migrazione aggiunge la colonna del conteggio con un valore predefinito. ```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 ``` Dopo questa configurazione, `article.comments_count` legge direttamente la colonna senza query SQL aggiuntive. ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

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

<% end %> ``` ## 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. ```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 ``` ### Scope riutilizzabili Centralizzare gli includes frequenti negli scope semplifica la manutenzione e garantisce coerenza. ```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 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 `includes` per impostazione predefinita, `eager_load` se 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 > **Attenzione al precaricamento eccessivo** > > 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:** - `includes` risolve la maggior parte dei casi N+1 precaricando le associazioni - `eager_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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems