ActiveRecord : Optimiser les requêtes N+1 dans Ruby on Rails
Guide complet pour détecter et corriger les problèmes de requêtes N+1 dans Rails avec ActiveRecord. Includes, preload, eager_load et outils de détection.

Les requêtes N+1 représentent l'un des problèmes de performance les plus courants dans les applications Rails. Une simple boucle sur des enregistrements peut déclencher des centaines de requêtes SQL inutiles, ralentissant drastiquement les temps de réponse. Ce guide couvre les techniques de détection et de résolution pour garantir des applications Rails performantes.
Une page affichant 50 articles avec leurs auteurs peut générer 51 requêtes SQL au lieu d'une seule. En production avec des milliers d'utilisateurs, ce problème devient critique pour les temps de réponse et la charge serveur.
Comprendre le problème N+1
Le problème N+1 survient lorsque le code effectue une requête pour récupérer une liste d'enregistrements (1 requête), puis exécute une requête supplémentaire pour chaque enregistrement afin d'accéder à ses associations (N requêtes). Le nom "N+1" décrit exactement ce pattern : 1 requête initiale + N requêtes pour les associations.
Prenons un exemple concret avec des articles et leurs auteurs. Sans optimisation, chaque accès à l'auteur d'un article déclenche une nouvelle requête SQL.
# app/controllers/articles_controller.rb
# Exemple de code générant un problème N+1
class ArticlesController < ApplicationController
def index
# 1 requête : SELECT * FROM articles
@articles = Article.all
end
endDans la vue, chaque appel à article.author déclenche une requête supplémentaire vers la base de données.
<!-- app/views/articles/index.html.erb -->
<!-- Cette vue génère N requêtes supplémentaires -->
<% @articles.each do |article| %>
<div class="article">
<h2><%= article.title %></h2>
<!-- Chaque appel génère : SELECT * FROM users WHERE id = ? -->
<p>Par <%= article.author.name %></p>
</div>
<% end %>Pour 100 articles, ce code génère 101 requêtes SQL. Les logs Rails montrent clairement le problème avec des requêtes répétitives.
-- Logs Rails montrant le problème N+1
-- 1 requête initiale
SELECT "articles".* FROM "articles"
-- N requêtes pour les auteurs (répétées pour chaque 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 requêtes supplémentairesRésoudre avec includes
La méthode includes est la solution la plus courante et la plus recommandée pour résoudre les problèmes N+1. Elle indique à ActiveRecord de précharger les associations en une ou deux requêtes optimisées.
# app/controllers/articles_controller.rb
# Solution avec includes - préchargement des auteurs
class ArticlesController < ApplicationController
def index
# Précharge les auteurs avec les articles
# Génère seulement 2 requêtes au lieu de N+1
@articles = Article.includes(:author).all
end
endAvec includes, ActiveRecord exécute seulement deux requêtes quelle que soit la quantité d'articles. La première récupère tous les articles, la seconde récupère tous les auteurs concernés.
-- Logs Rails avec includes (seulement 2 requêtes)
SELECT "articles".* FROM "articles"
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...)Les associations imbriquées peuvent également être préchargées en utilisant une syntaxe en hash. Cette approche est essentielle lorsque les vues accèdent à plusieurs niveaux d'associations.
# app/controllers/articles_controller.rb
# Préchargement d'associations imbriquées
class ArticlesController < ApplicationController
def index
# Précharge author -> company et tous les comments
@articles = Article.includes(author: :company, comments: :user)
end
endSi une vue accède à une association dans une boucle, cette association doit être préchargée dans le contrôleur avec includes. Vérifier systématiquement les accès aux associations dans les vues.
Différences entre includes, preload et eager_load
Rails propose trois méthodes pour le préchargement des associations. Chacune utilise une stratégie SQL différente, avec des cas d'usage spécifiques.
preload : requêtes séparées
La méthode preload exécute toujours des requêtes séparées pour chaque association. Elle est efficace quand aucune condition WHERE ne filtre sur les associations.
# app/models/article.rb
# preload utilise toujours des requêtes séparées
class Article < ApplicationRecord
scope :with_authors, -> { preload(:author) }
end
# Utilisation dans le controller
@articles = Article.with_authors.limit(20)
# SQL généré :
# SELECT "articles".* FROM "articles" LIMIT 20
# SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, ...)eager_load : LEFT OUTER JOIN
La méthode eager_load utilise un LEFT OUTER JOIN pour charger les données en une seule requête. Elle est obligatoire pour filtrer ou trier sur les colonnes des associations.
# app/controllers/articles_controller.rb
# eager_load permet de filtrer sur les associations
class ArticlesController < ApplicationController
def verified_authors
# Filtre les articles par statut de l'auteur
# Nécessite eager_load car WHERE porte sur users
@articles = Article.eager_load(:author)
.where(users: { verified: true })
.order("users.name ASC")
end
end
# SQL généré (une seule requête avec 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 : comportement intelligent
La méthode includes choisit automatiquement la meilleure stratégie. Elle utilise preload par défaut, mais bascule vers eager_load si une clause WHERE référence l'association.
# app/controllers/articles_controller.rb
# includes s'adapte automatiquement au contexte
class ArticlesController < ApplicationController
def index
# Sans condition sur l'association : utilise preload (2 requêtes)
@articles = Article.includes(:author).all
end
def by_verified_authors
# Avec condition sur l'association : utilise eager_load (JOIN)
@articles = Article.includes(:author)
.where(users: { verified: true })
end
endLe tableau suivant résume les différences entre les trois méthodes.
| Méthode | Stratégie SQL | Cas d'usage |
|---------|---------------|-------------|
| preload | Requêtes séparées | Préchargement simple, pas de filtrage |
| eager_load | LEFT OUTER JOIN | Filtrage/tri sur associations |
| includes | Automatique | Usage général, recommandé par défaut |
Détecter les requêtes N+1 automatiquement
La détection manuelle des problèmes N+1 est fastidieuse et sujette aux oublis. Plusieurs outils automatisent cette détection en développement et en CI.
Bullet : détection en temps réel
La gem Bullet analyse les requêtes SQL en temps réel et alerte sur les problèmes N+1 détectés. Elle propose également les corrections appropriées.
# Gemfile
# Bullet détecte les N+1 en développement
group :development do
gem 'bullet'
endLa configuration dans l'environnement de développement active les différents modes d'alerte.
# config/environments/development.rb
# Configuration de Bullet pour détecter les N+1
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
# Affiche une alerte JavaScript dans le navigateur
Bullet.alert = true
# Ajoute un footer avec les détails
Bullet.bullet_logger = true
# Affiche dans les logs Rails
Bullet.rails_logger = true
# Lève une exception (utile en CI)
Bullet.raise = false
end
endLorsqu'un problème N+1 est détecté, Bullet affiche un message explicite avec la solution recommandée.
# Exemple d'alerte Bullet dans les logs
USE eager loading detected
Article => [:author]
Add to your query: .includes([:author])
Call stack:
/app/views/articles/index.html.erb:5En intégration continue, activer Bullet.raise = true permet de faire échouer les tests si un problème N+1 est détecté. Cela empêche les régressions de performance.
Prosopite : alternative légère
La gem Prosopite offre une alternative plus légère à Bullet, avec une configuration minimale et une compatibilité avec les tests.
# Gemfile
# Prosopite comme alternative à Bullet
group :development, :test do
gem 'prosopite'
end# config/environments/development.rb
# Configuration Prosopite
Rails.application.configure do
config.after_initialize do
Prosopite.rails_logger = true
Prosopite.raise = Rails.env.test?
end
endTechniques avancées d'optimisation
Au-delà des méthodes de base, plusieurs techniques permettent d'optimiser finement les requêtes ActiveRecord.
Strict loading : prévention par défaut
Rails 6.1+ propose le mode strict loading qui lève une exception si une association non préchargée est accédée. Cette approche préventive force à résoudre les N+1 dès le développement.
# app/models/article.rb
# Active le strict loading par défaut sur le modèle
class Article < ApplicationRecord
# Toute association non préchargée lèvera une exception
self.strict_loading_by_default = true
belongs_to :author
has_many :comments
endLe strict loading peut aussi être activé ponctuellement sur une requête spécifique.
# app/controllers/articles_controller.rb
# Strict loading ponctuel sur une requête
class ArticlesController < ApplicationController
def index
# Lève StrictLoadingViolationError si une association
# non incluse est accédée
@articles = Article.strict_loading.includes(:author)
end
endSelect et pluck pour les données partielles
Quand seules certaines colonnes sont nécessaires, select et pluck réduisent la quantité de données transférées depuis la base.
# app/controllers/reports_controller.rb
# Optimisation avec select et pluck
class ReportsController < ApplicationController
def titles_only
# select retourne des objets Article avec seulement id et title
@articles = Article.select(:id, :title)
end
def title_array
# pluck retourne un Array de valeurs, pas d'objets AR
# Plus performant quand seules les valeurs sont nécessaires
@titles = Article.pluck(:title)
# => ["Premier article", "Second article", ...]
end
endCounter cache pour les comptages
Les comptages d'associations (article.comments.count) génèrent une requête SQL à chaque appel. Le counter cache stocke ce comptage directement dans la table parente.
# app/models/comment.rb
# Configuration du counter cache
class Comment < ApplicationRecord
# Rails maintient automatiquement le compteur dans articles.comments_count
belongs_to :article, counter_cache: true
endLa migration ajoute la colonne de comptage avec une valeur par défaut.
# db/migrate/20260223_add_comments_count_to_articles.rb
# Migration pour ajouter le counter cache
class AddCommentsCountToArticles < ActiveRecord::Migration[7.1]
def change
add_column :articles, :comments_count, :integer, default: 0, null: false
# Initialise les compteurs pour les données existantes
Article.find_each do |article|
Article.reset_counters(article.id, :comments)
end
end
endAprès cette configuration, article.comments_count lit directement la colonne sans requête SQL supplémentaire.
# app/views/articles/index.html.erb
# Utilisation du counter cache (pas de requête SQL)
<% @articles.each do |article| %>
<p><%= article.title %> - <%= article.comments_count %> commentaires</p>
<% end %>Prêt à réussir tes entretiens Ruby on Rails ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Bonnes pratiques et checklist
Une approche systématique permet d'éviter les problèmes N+1 dans les nouveaux développements et de corriger progressivement le code existant.
Analyse des vues avant le code
Avant d'écrire le code d'un contrôleur, analyser la vue pour identifier toutes les associations accédées. Cette anticipation évite les oublis.
# app/controllers/articles_controller.rb
# Analyse préalable de la vue pour identifier les includes nécessaires
class ArticlesController < ApplicationController
def show
# La vue accède à : author, author.company, comments, comments.user
# Tous ces éléments doivent être préchargés
@article = Article.includes(
author: :company,
comments: :user
).find(params[:id])
end
endScopes réutilisables
Centraliser les includes fréquents dans des scopes facilite la maintenance et garantit la cohérence.
# app/models/article.rb
# Scopes réutilisables pour le préchargement
class Article < ApplicationRecord
# Scope pour l'affichage en liste
scope :with_author, -> { includes(:author) }
# Scope pour l'affichage détaillé
scope :with_full_details, -> {
includes(
author: :company,
comments: { user: :avatar_attachment },
tags: []
)
}
# Scope pour l'admin avec toutes les relations
scope :for_admin, -> {
includes(:author, :comments, :tags, :category)
.with_attached_cover_image
}
endChecklist de prévention
Cette checklist résume les points de vérification essentiels pour éviter les problèmes N+1 :
- Installer et configurer Bullet ou Prosopite en développement
- Activer Bullet.raise en CI pour bloquer les régressions
- Analyser les vues pour identifier les associations avant d'écrire les contrôleurs
- Utiliser
includespar défaut,eager_loadsi filtrage sur associations - Créer des scopes réutilisables pour les patterns de préchargement fréquents
- Utiliser le strict loading sur les modèles sensibles
- Ajouter des counter caches pour les comptages fréquents
- Vérifier les logs SQL en développement régulièrement
Précharger trop d'associations consomme de la mémoire inutilement. Précharger uniquement ce qui est réellement utilisé dans la vue. Les outils comme Bullet détectent aussi le "unused eager loading".
Conclusion
Les requêtes N+1 constituent un problème de performance majeur mais facilement évitable dans les applications Rails. Une combinaison d'outils de détection automatique et de bonnes pratiques de développement permet d'éliminer ce problème.
Points clés à retenir :
includesrésout la majorité des cas N+1 en préchargeant les associationseager_loadest nécessaire pour filtrer ou trier sur les associations- Bullet et Prosopite détectent automatiquement les problèmes en développement
- Le strict loading prévient les N+1 en levant des exceptions
- Les counter caches optimisent les comptages fréquents
- Analyser les vues avant d'écrire les contrôleurs évite les oublis
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Questions d'entretien Ruby on Rails : Top 25 en 2026
Les 25 questions d'entretien Ruby on Rails les plus posées. Architecture MVC, Active Record, migrations, tests RSpec, API REST avec réponses détaillées et exemples de code.

Ruby on Rails 7 : Hotwire et Turbo pour des applications réactives
Guide complet sur Hotwire et Turbo dans Rails 7. Apprenez à créer des applications réactives sans écrire de JavaScript avec Turbo Drive, Frames et Streams.

Action Cable et WebSockets dans Rails : Guide Complet pour les Entretiens Techniques
Action Cable integre les WebSockets directement dans Rails. Ce guide approfondi couvre l'architecture des connexions, les channels, Solid Cable, Turbo Streams, le scaling avec Redis et les patterns de test pour les entretiens techniques.