ActiveRecord: N+1-queryproblemen oplossen in Ruby on Rails
Volledige gids voor het detecteren en oplossen van N+1-queries in Rails met ActiveRecord. Beheers includes, preload, eager_load en geautomatiseerde detectietools.

N+1-queries vormen een van de meest voorkomende prestatieproblemen in Rails-applicaties. Een eenvoudige loop over records kan honderden onnodige SQL-queries veroorzaken en de responstijden drastisch vertragen. Deze gids behandelt detectie- en oplostechnieken om performante Rails-applicaties te garanderen.
Een pagina met 50 artikelen en hun auteurs kan 51 SQL-queries genereren in plaats van slechts één. In productie met duizenden gebruikers wordt dit probleem kritiek voor responstijden en serverbelasting.
Het N+1-probleem begrijpen
Het N+1-probleem treedt op wanneer de code één query uitvoert om een lijst met records op te halen (1 query) en vervolgens een extra query uitvoert voor elk record om toegang te krijgen tot de bijbehorende associaties (N queries). De naam "N+1" beschrijft precies dit patroon: 1 initiële query + N queries voor de associaties.
Een concreet voorbeeld met artikelen en hun auteurs maakt het probleem duidelijk. Zonder optimalisatie veroorzaakt elke toegang tot de auteur van een artikel een nieuwe SQL-query.
# 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
endIn de view veroorzaakt elke aanroep van article.author een extra query naar de 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 %>Voor 100 artikelen genereert deze code 101 SQL-queries. De Rails-logs tonen het probleem duidelijk via repetitieve queries.
-- 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 queriesOplossen met includes
De methode includes is de meest gangbare en aanbevolen oplossing voor het verhelpen van N+1-problemen. Ze geeft ActiveRecord opdracht om de associaties vooraf te laden in één of twee geoptimaliseerde queries.
# 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
endMet includes voert ActiveRecord slechts twee queries uit, ongeacht het aantal artikelen. De eerste haalt alle artikelen op, de tweede haalt alle relevante auteurs op.
-- Rails logs with includes (only 2 queries)
SELECT "articles".* FROM "articles"
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...)Geneste associaties kunnen ook vooraf worden geladen met hash-syntaxis. Deze aanpak is essentieel wanneer views toegang hebben tot meerdere niveaus van associaties.
# 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
endAls een view binnen een loop toegang heeft tot een associatie, moet die associatie in de controller vooraf worden geladen met includes. Het is belangrijk om altijd de toegangspatronen tot associaties in de views te controleren.
Verschillen tussen includes, preload en eager_load
Rails biedt drie methoden om associaties vooraf te laden. Elk gebruikt een andere SQL-strategie, met specifieke gebruiksgevallen.
preload: aparte queries
De methode preload voert altijd aparte queries uit voor elke associatie. Ze werkt efficiënt wanneer geen WHERE-conditie filtert op de associaties.
# 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
De methode eager_load gebruikt een LEFT OUTER JOIN om de gegevens in één query te laden. Ze wordt verplicht bij het filteren of sorteren op kolommen van associaties.
# 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: slim gedrag
De methode includes kiest automatisch de beste strategie. Standaard gebruikt ze preload, maar schakelt over op eager_load als een WHERE-clausule naar de associatie verwijst.
# 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
endDe volgende tabel vat de verschillen tussen de drie methoden samen.
| Methode | SQL-strategie | Toepassing |
|---------|---------------|------------|
| preload | Aparte queries | Eenvoudige preloading, geen filtering |
| eager_load | LEFT OUTER JOIN | Filteren/sorteren op associaties |
| includes | Automatisch | Algemeen gebruik, aanbevolen standaard |
Geautomatiseerde N+1-detectie
Het handmatig opsporen van N+1-problemen is moeizaam en foutgevoelig. Verschillende tools automatiseren deze detectie in development en CI.
Bullet: realtime detectie
De gem Bullet analyseert SQL-queries in realtime en waarschuwt voor gedetecteerde N+1-problemen. Daarnaast stelt ze passende oplossingen voor.
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
endDe configuratie in de development-omgeving maakt het mogelijk om verschillende waarschuwingsmodi in te schakelen.
# 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
endWanneer een N+1-probleem wordt gedetecteerd, toont Bullet een duidelijke melding met de aanbevolen oplossing.
# 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 continuous integration zorgt het inschakelen van Bullet.raise = true ervoor dat tests falen als een N+1-probleem wordt gedetecteerd. Zo worden prestatieregressies voorkomen.
Prosopite: lichtgewicht alternatief
De gem Prosopite biedt een lichter alternatief voor Bullet, met minimale configuratie en compatibiliteit met tests.
# 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
endGeavanceerde optimalisatietechnieken
Naast de basismethoden zijn er meerdere technieken om de optimalisatie van ActiveRecord-queries fijn af te stemmen.
Strict Loading: preventie als standaard
Rails 6.1+ biedt een strict-loadingmodus die een uitzondering oproept als er toegang wordt verkregen tot een niet vooraf geladen associatie. Deze preventieve aanpak dwingt om N+1's tijdens de ontwikkeling op te lossen.
# 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
endStrict loading kan ook per specifieke query worden ingeschakeld.
# 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 en Pluck voor gedeeltelijke gegevens
Wanneer slechts bepaalde kolommen nodig zijn, verminderen select en pluck de hoeveelheid gegevens die uit de database wordt overgedragen.
# 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 voor tellingen
Associatietellingen (article.comments.count) genereren bij elke aanroep een SQL-query. Counter cache slaat deze telling rechtstreeks op in de bovenliggende tabel.
# 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
endDe migratie voegt de tellingskolom toe met een standaardwaarde.
# 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
endNa deze configuratie leest article.comments_count de kolom rechtstreeks zonder extra SQL-queries.
# 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 %>Klaar om je Ruby on Rails gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Best practices en checklist
Een systematische aanpak voorkomt N+1-problemen bij nieuwe ontwikkelingen en lost bestaande code geleidelijk op.
View-analyse vóór de code
Voordat de controllercode wordt geschreven, is het verstandig om de view te analyseren om alle benaderde associaties te identificeren. Deze vroege inventarisatie voorkomt omissies.
# 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
endHerbruikbare scopes
Frequente includes centraliseren in scopes vereenvoudigt het onderhoud en zorgt voor consistentie.
# 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
}
endPreventieve checklist
Deze checklist vat de essentiële controlepunten samen om N+1-problemen te vermijden:
- Bullet of Prosopite installeren en configureren in development
- Bullet.raise inschakelen in CI om regressies te blokkeren
- Views analyseren om associaties te identificeren voordat controllers worden geschreven
- Standaard
includesgebruiken,eager_loadbij filtering op associaties - Herbruikbare scopes maken voor frequente preloadingpatronen
- Strict loading toepassen op gevoelige modellen
- Counter caches toevoegen voor frequente tellingen
- Regelmatig de SQL-logs in development controleren
Te veel associaties vooraf laden verbruikt onnodig geheugen. Alleen associaties die de view daadwerkelijk gebruikt, dienen te worden vooraf geladen. Tools zoals Bullet detecteren ook "unused eager loading".
Conclusie
N+1-queries vormen een belangrijk prestatieprobleem dat in Rails-applicaties eenvoudig te voorkomen is. De combinatie van geautomatiseerde detectietools en goede ontwikkelpraktijken elimineert dit probleem effectief.
Belangrijkste punten:
includeslost de meeste N+1-gevallen op door associaties vooraf te ladeneager_loadis vereist bij het filteren of sorteren op associaties- Bullet en Prosopite detecteren problemen automatisch in development
- Strict loading voorkomt N+1's door uitzonderingen op te roepen
- Counter caches optimaliseren frequente tellingen
- Views analyseren vóór het schrijven van controllers voorkomt omissies
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

Ruby on Rails sollicitatievragen: Top 25 in 2026
De 25 meest gestelde Ruby on Rails sollicitatievragen. MVC-architectuur, Active Record, migraties, RSpec-testing, REST-APIs met gedetailleerde antwoorden en codevoorbeelden.

Ruby on Rails 7: Hotwire en Turbo voor Reactieve Applicaties
Volledige gids over Hotwire en Turbo in Rails 7. Bouw reactieve applicaties zonder JavaScript met Turbo Drive, Frames en Streams.

Rails API-modus in 2026: RESTful API's bouwen, JSON-serialisatie en sollicitatievragen
Praktische handleiding voor Rails 8.1 API-modus: RESTful routeontwerp, serialisatie met Alba en jsonapi-serializer, JWT-authenticatie, gecentraliseerde foutafhandeling, paginering en veelgestelde sollicitatievragen voor Ruby on Rails API-ontwikkelaars.