# ActiveRecord: rozwiązywanie problemów z zapytaniami N+1 w Ruby on Rails > Kompletny przewodnik po wykrywaniu i naprawianiu zapytań N+1 w Rails z ActiveRecord. Opanuj includes, preload, eager_load i narzędzia automatycznej detekcji. - Published: 2026-02-23 - Updated: 2026-04-29 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, activerecord, performance, n+1 queries, sql optimization - Reading time: 10 min --- Zapytania N+1 stanowią jeden z najczęstszych problemów wydajnościowych w aplikacjach Rails. Prosta pętla po rekordach może wywołać setki zbędnych zapytań SQL, drastycznie spowalniając czasy odpowiedzi. Ten przewodnik obejmuje techniki wykrywania i rozwiązywania, aby zapewnić wydajność aplikacji Rails. > **Wpływ na produkcję** > > Strona wyświetlająca 50 artykułów wraz z ich autorami może wygenerować 51 zapytań SQL zamiast jednego. Na produkcji z tysiącami użytkowników problem ten staje się krytyczny dla czasów odpowiedzi i obciążenia serwera. ## Zrozumienie problemu N+1 Problem N+1 występuje, gdy kod wykonuje jedno zapytanie do pobrania listy rekordów (1 zapytanie), a następnie uruchamia dodatkowe zapytanie dla każdego rekordu w celu uzyskania dostępu do jego asocjacji (N zapytań). Nazwa "N+1" opisuje dokładnie ten wzorzec: 1 zapytanie początkowe + N zapytań dla asocjacji. Konkretny przykład z artykułami i ich autorami obrazuje problem. Bez optymalizacji każdy dostęp do autora artykułu wywołuje nowe zapytanie SQL. ```ruby # 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 end ``` W widoku każde wywołanie `article.author` powoduje dodatkowe zapytanie do bazy danych. ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` Dla 100 artykułów ten kod generuje 101 zapytań SQL. Logi Rails wyraźnie pokazują problem poprzez powtarzające się zapytania. ```sql -- 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 queries ``` ## Rozwiązanie z użyciem includes Metoda `includes` to najczęstsze i zalecane rozwiązanie problemów N+1. Instruuje ona ActiveRecord, aby wstępnie załadował asocjacje w jednym lub dwóch zoptymalizowanych zapytaniach. ```ruby # 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 end ``` Z `includes` ActiveRecord wykonuje tylko dwa zapytania niezależnie od liczby artykułów. Pierwsze pobiera wszystkie artykuły, drugie pobiera wszystkich istotnych autorów. ```sql -- Rails logs with includes (only 2 queries) SELECT "articles".* FROM "articles" SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...) ``` Zagnieżdżone asocjacje również można wstępnie ładować przy użyciu składni hash. To podejście jest niezbędne, gdy widoki uzyskują dostęp do wielu poziomów asocjacji. ```ruby # 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 end ``` > **Złota zasada** > > Jeśli widok uzyskuje dostęp do asocjacji wewnątrz pętli, ta asocjacja musi być wstępnie załadowana w kontrolerze za pomocą `includes`. Należy zawsze weryfikować wzorce dostępu do asocjacji w widokach. ## Różnice między includes, preload i eager_load Rails udostępnia trzy metody wstępnego ładowania asocjacji. Każda z nich wykorzystuje inną strategię SQL i ma swoje konkretne zastosowania. ### preload: oddzielne zapytania Metoda `preload` zawsze wykonuje oddzielne zapytania dla każdej asocjacji. Działa wydajnie, gdy żaden warunek WHERE nie filtruje po asocjacjach. ```ruby # 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 Metoda `eager_load` używa LEFT OUTER JOIN, aby załadować dane w jednym zapytaniu. Staje się obowiązkowa przy filtrowaniu lub sortowaniu po kolumnach asocjacji. ```ruby # 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" ASC ``` ### includes: inteligentne zachowanie Metoda `includes` automatycznie wybiera najlepszą strategię. Domyślnie używa `preload`, ale przełącza się na `eager_load`, jeśli klauzula WHERE odwołuje się do asocjacji. ```ruby # 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 end ``` Poniższa tabela podsumowuje różnice między trzema metodami. | Metoda | Strategia SQL | Zastosowanie | |--------|---------------|--------------| | `preload` | Oddzielne zapytania | Proste wstępne ładowanie, bez filtrowania | | `eager_load` | LEFT OUTER JOIN | Filtrowanie/sortowanie po asocjacjach | | `includes` | Automatyczna | Ogólne zastosowanie, zalecany domyślny wybór | ## Zautomatyzowana detekcja N+1 Ręczne wykrywanie problemów N+1 jest żmudne i podatne na błędy. Kilka narzędzi automatyzuje tę detekcję w środowisku deweloperskim i CI. ### Bullet: detekcja w czasie rzeczywistym Gem Bullet analizuje zapytania SQL w czasie rzeczywistym i ostrzega o wykrytych problemach N+1. Sugeruje również odpowiednie poprawki. ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` Konfiguracja w środowisku deweloperskim umożliwia włączenie różnych trybów alertów. ```ruby # 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 end ``` Gdy wykryty zostaje problem N+1, Bullet wyświetla jednoznaczny komunikat z zalecanym rozwiązaniem. ```text # 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:5 ``` > **Bullet w CI** > > W ciągłej integracji włączenie `Bullet.raise = true` powoduje, że testy zakończą się niepowodzeniem przy wykryciu problemu N+1. Zapobiega to regresji wydajności. ### Prosopite: lekka alternatywa Gem Prosopite oferuje lżejszą alternatywę dla Bullet, z minimalną konfiguracją i kompatybilnością z testami. ```ruby # Gemfile # Prosopite as an alternative to Bullet group :development, :test do gem 'prosopite' end ``` ```ruby # config/environments/development.rb # Prosopite configuration Rails.application.configure do config.after_initialize do Prosopite.rails_logger = true Prosopite.raise = Rails.env.test? end end ``` ## Zaawansowane techniki optymalizacji Oprócz podstawowych metod kilka technik pozwala na precyzyjne dostrajanie optymalizacji zapytań ActiveRecord. ### Strict Loading: prewencja domyślna Rails 6.1+ udostępnia tryb ścisłego ładowania, który zgłasza wyjątek przy próbie dostępu do nieprealadowanej asocjacji. To prewencyjne podejście wymusza rozwiązywanie N+1 już w trakcie programowania. ```ruby # 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 end ``` Ścisłe ładowanie można też włączyć dla konkretnego zapytania. ```ruby # 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 end ``` ### Select i Pluck dla danych częściowych Gdy potrzebne są tylko niektóre kolumny, `select` i `pluck` zmniejszają ilość danych przesyłanych z bazy. ```ruby # 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 end ``` ### Counter Cache do zliczeń Zliczenia asocjacji (`article.comments.count`) generują zapytanie SQL przy każdym wywołaniu. Counter cache przechowuje to zliczenie bezpośrednio w tabeli nadrzędnej. ```ruby # 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 end ``` Migracja dodaje kolumnę zliczenia z wartością domyślną. ```ruby # 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 end ``` Po tej konfiguracji `article.comments_count` odczytuje kolumnę bezpośrednio, bez dodatkowych zapytań SQL. ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

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

<% end %> ``` ## Najlepsze praktyki i checklista Systematyczne podejście zapobiega problemom N+1 w nowych projektach i stopniowo poprawia istniejący kod. ### Analiza widoku przed kodem Przed napisaniem kodu kontrolera warto przeanalizować widok, aby zidentyfikować wszystkie używane asocjacje. Takie wyprzedzenie zapobiega pominięciom. ```ruby # 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 end ``` ### Wielokrotnego użytku scopy Centralizacja częstych includes w scopach upraszcza utrzymanie i zapewnia spójność. ```ruby # 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 } end ``` ### Checklista prewencyjna Ta checklista podsumowuje kluczowe punkty kontroli, które pomagają unikać problemów N+1: - Zainstalować i skonfigurować Bullet lub Prosopite w środowisku deweloperskim - Włączyć Bullet.raise w CI, aby blokować regresje - Analizować widoki w celu identyfikacji asocjacji przed pisaniem kontrolerów - Domyślnie używać `includes`, `eager_load` przy filtrowaniu po asocjacjach - Tworzyć wielokrotnego użytku scopy dla częstych wzorców prelaadingu - Stosować strict loading na wrażliwych modelach - Dodawać counter cache dla częstych zliczeń - Regularnie sprawdzać logi SQL w środowisku deweloperskim > **Uwaga na nadmierne preloadowanie** > > Preloadowanie zbyt wielu asocjacji niepotrzebnie zużywa pamięć. Należy preloadować jedynie asocjacje rzeczywiście używane przez widok. Narzędzia takie jak Bullet wykrywają również "unused eager loading". ## Podsumowanie Zapytania N+1 to istotny problem wydajnościowy, łatwy do uniknięcia w aplikacjach Rails. Połączenie zautomatyzowanych narzędzi detekcji i dobrych praktyk programistycznych skutecznie eliminuje ten problem. **Najważniejsze wnioski:** - `includes` rozwiązuje większość przypadków N+1 poprzez wstępne ładowanie asocjacji - `eager_load` jest wymagany przy filtrowaniu lub sortowaniu po asocjacjach - Bullet i Prosopite automatycznie wykrywają problemy w środowisku deweloperskim - Strict loading zapobiega N+1 poprzez zgłaszanie wyjątków - Counter cache optymalizuje częste zliczenia - Analiza widoków przed pisaniem kontrolerów zapobiega pominięciom --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems