# 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. - Published: 2026-02-23 - Updated: 2026-03-28 - Author: SharpSkill - Tags: ruby on rails, activerecord, performance, requêtes n+1, optimisation sql - Reading time: 10 min --- 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. > **Impact en production** > > 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. ```ruby # 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 end ``` Dans la vue, chaque appel à `article.author` déclenche une requête supplémentaire vers la base de données. ```erb <% @articles.each do |article| %>

<%= article.title %>

Par <%= article.author.name %>

<% 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. ```sql -- 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émentaires ``` ## Ré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. ```ruby # 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 end ``` Avec `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. ```sql -- 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. ```ruby # 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 end ``` > **Règle d'or** > > Si 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. ```ruby # 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. ```ruby # 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" ASC ``` ### includes : 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. ```ruby # 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 end ``` Le 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. ```ruby # Gemfile # Bullet détecte les N+1 en développement group :development do gem 'bullet' end ``` La configuration dans l'environnement de développement active les différents modes d'alerte. ```ruby # 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 end ``` Lorsqu'un problème N+1 est détecté, Bullet affiche un message explicite avec la solution recommandée. ```text # 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:5 ``` > **Bullet en CI** > > En 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. ```ruby # Gemfile # Prosopite comme alternative à Bullet group :development, :test do gem 'prosopite' end ``` ```ruby # config/environments/development.rb # Configuration Prosopite Rails.application.configure do config.after_initialize do Prosopite.rails_logger = true Prosopite.raise = Rails.env.test? end end ``` ## Techniques 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. ```ruby # 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 end ``` Le strict loading peut aussi être activé ponctuellement sur une requête spécifique. ```ruby # 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 end ``` ### Select 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. ```ruby # 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 end ``` ### Counter 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. ```ruby # 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 end ``` La migration ajoute la colonne de comptage avec une valeur par défaut. ```ruby # 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 end ``` Après cette configuration, `article.comments_count` lit directement la colonne sans requête SQL supplémentaire. ```ruby # app/views/articles/index.html.erb # Utilisation du counter cache (pas de requête SQL) <% @articles.each do |article| %>

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

<% end %> ``` ## 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. ```ruby # 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 end ``` ### Scopes réutilisables Centraliser les includes fréquents dans des scopes facilite la maintenance et garantit la cohérence. ```ruby # 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 } end ``` ### Checklist 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 `includes` par défaut, `eager_load` si 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 > **Attention au sur-préchargement** > > 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 :** - `includes` résout la majorité des cas N+1 en préchargeant les associations - `eager_load` est 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ruby-on-rails/activerecord-optimiser-requetes-n-plus-1