17. September 2026
@posts.each { |post| post.author.name } sieht harmlos aus. Im Code-Review fällt es niemandem auf, im lokalen Development mit zwölf Testdatensätzen merkt es niemand, und in der Testsuite ist es unsichtbar, weil dort selten mit produktionsnahen Datenmengen gearbeitet wird. In Produktion mit dreitausend Posts sind es dreitausendundein Query für das, was eigentlich zwei sein sollten.
Das ist die klassische N+1-Query – und sie ist deshalb so hartnäckig, weil sie kein Symptom von schlechtem Code ist. Sie ist ein Symptom von gutem ActiveRecord-Code, der eine Grenze überschreitet, die im Code selbst unsichtbar bleibt: die Grenze zwischen Ruby-Objekt und Datenbankzugriff.
Warum Code Review sie nicht fängt
post.author.name ist, isoliert betrachtet, korrekter, idiomatischer Rails-Code. Ob er einen einzelnen Datenbankzugriff auslöst oder tausend, hängt ausschließlich davon ab, was vorher passiert ist – ob die Assoziation bereits geladen (includes, preload) oder nur referenziert wurde. Diese Information steht nicht an der Stelle, an der der Fehler entsteht. Ein Reviewer, der nur den Diff sieht, kann die Kosten einer einzelnen Zeile nicht beurteilen, ohne den kompletten Aufrufkontext zu kennen. Das ist der Grund, warum N+1-Probleme in Pull-Request-Reviews fast nie auffallen, egal wie erfahren das Team ist.
Warum lokale Entwicklung sie versteckt
Bei zwölf Datensätzen ist der Unterschied zwischen einem Query und dreizehn Queries nicht messbar – beide fühlen sich "sofort" an. Das Problem skaliert nicht mit der Codequalität, sondern mit der Datenmenge, und Entwicklungsumgebungen haben strukturell zu wenig davon. Deshalb ist "es lief lokal schnell" hier ein besonders schlechter Indikator: Die Zeile Code, die lokal 3 Millisekunden braucht, kann in Produktion 3 Sekunden brauchen, ohne dass sich an der Zeile selbst etwas geändert hat.
Wie man sie systematisch findet, statt sie zu erraten
Drei Techniken, die ich in Reihenfolge steigenden Aufwands einsetze:
1. Bullet oder rack-mini-profiler im Entwicklungsmodus mitlaufen lassen. Beide Gems erkennen N+1-Zugriffsmuster automatisch und melden sie im Browser oder Log – vorausgesetzt, man klickt sich tatsächlich durch Ansichten mit realistischen Datenmengen, nicht nur durch leere Seeds.
2. ActiveRecord::Base.logger = Logger.new(STDOUT) in einer Staging-Umgebung mit produktionsnahem Datenvolumen. Wiederkehrende, fast identische SQL-Statements mit unterschiedlichen IDs im Log sind der eindeutigste Fingerabdruck eines N+1-Problems – auffälliger als jedes Tool, weil man die Wiederholung mit eigenen Augen zählt.
3. strict_loading! gezielt auf einzelnen Modellen aktivieren. Rails wirft dann eine Exception, sobald eine nicht geladene Assoziation lazy nachgeladen wird. Das ist aggressiv und für den produktiven Einsatz auf einem ganzen Modell oft zu riskant – aber als temporäres Diagnosewerkzeug in einem einzelnen Controller-Test deckt es jeden versteckten Zugriff zuverlässig auf, ganz ohne Rätselraten.
Der Konsulting-Punkt: nicht jedes N+1 lohnt den Fix
Nicht jede gefundene N+1-Query rechtfertigt sofortigen Handlungsbedarf. Eine Admin-Seite, die zweimal am Tag von einer Person aufgerufen wird, darf ineffizient bleiben – die Entwicklerzeit ist anderswo wertvoller. Der öffentliche Feed, der bei jedem Seitenaufruf jedes zahlenden Kunden läuft, nicht. Der relevante Maßstab ist nicht "Anzahl der Queries", sondern "Traffic × Kosten pro Query" – und genau diese Priorisierung ist der Teil, der in einem Audit tatsächlich Zeit spart, weil er verhindert, dass ein Team drei Wochen in Performance-Arbeit an Code investiert, den ohnehin kaum jemand aufruft.