Section 13.3.4 : Requêtes avancées et optimisation
🎯 Objectif pédagogique
Maîtriser les requêtes SQL avancées (subqueries, window functions) et comprendre l'optimisation des performances avec Active Record (N+1, eager loading, indices).
Le problème N+1 : l'ennemi des performances
Le problème N+1 est le piège le plus courant en Active Record. Il se produit quand vous exécutez 1 requête pour récupérer N objets, puis N requêtes supplémentaires pour leurs associations.
# BAD : N+1 queries (1 + 10 = 11 requêtes)
articles = Article.limit(10)
articles.each do |article|
puts article.user.nom # Chaque appel fait une requête SQL !
end
# GOOD : Eager loading (2 requêtes seulement)
articles = Article.includes(:user).limit(10)
articles.each do |article|
puts article.user.nom # Déjà chargé en mémoire
end
Eager loading : includes, preload, eager_load
# includes : utilise preload ou eager_load selon le cas
Article.includes(:user, :tags).where(published: true)
# preload : 2+ requêtes séparées
Article.preload(:user).limit(10)
# SELECT * FROM articles LIMIT 10
# SELECT * FROM users WHERE id IN (1, 2, 3, ...)
# eager_load : LEFT OUTER JOIN (une seule requête)
Article.eager_load(:user).where(users: { ville: 'Paris' })
# SELECT articles.*, users.* FROM articles
# LEFT OUTER JOIN users ON users.id = articles.user_id
# WHERE users.ville = 'Paris'
Scopes : requêtes réutilisables
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_city, ->(city) { joins(:user).where(users: { ville: city }) }
scope :popular, -> { where('views_count > ?', 100) }
# Equivalent à des méthodes de classe
def self.trending
published.recent.popular.limit(10)
end
end
# Chaînable
Article.published.recent.limit(5)
Article.by_city('Paris').published
Article.trending
Indices : accélérer les requêtes
Un index est comme l'index d'un livre — il permet à la base de trouver rapidement une ligne sans scanner toute la table.
class AddIndicesToArticles < ActiveRecord::Migration[7.1]
def change
add_index :articles, :user_id # Foreign key
add_index :articles, :published # Colonnes fréquemment filtrées
add_index :articles, :created_at # Colonnes triées
add_index :articles, [:user_id, :published] # Index composite
add_index :users, :email, unique: true # Index unique
end
end
Subqueries et SQL avancé
# Subquery : trouver les users qui ont au moins un article
User.where(id: Article.select(:user_id).distinct)
# SQL brut quand nécessaire
User.find_by_sql("SELECT users.*, COUNT(articles.id) as articles_count
FROM users
LEFT JOIN articles ON articles.user_id = users.id
GROUP BY users.id
HAVING COUNT(articles.id) > 5")
# select pour ajouter des colonnes calculées
User.select('users.*, COUNT(articles.id) as articles_count')
.joins(:articles)
.group('users.id')
.order('articles_count DESC')
Comment résoudre un problème N+1 en Active Record ?
Apprendre l’IA, du premier prompt aux agents