# ActiveRecord: Ruby on Rails'te N+1 Sorgu Sorunlarını Çözmek > Rails'te ActiveRecord ile N+1 sorgularını tespit etme ve çözme rehberi. includes, preload, eager_load ve otomatik tespit araçlarına hâkim olun. - 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 --- N+1 sorguları, Rails uygulamalarındaki en yaygın performans sorunlarından birini oluşturur. Kayıtlar üzerinde basit bir döngü, yüzlerce gereksiz SQL sorgusunu tetikleyebilir ve yanıt sürelerini önemli ölçüde yavaşlatabilir. Bu rehber, performanslı Rails uygulamaları için tespit ve çözüm tekniklerini ele alır. > **Üretim üzerindeki etki** > > 50 makaleyi yazarlarıyla birlikte gösteren bir sayfa, tek bir sorgu yerine 51 SQL sorgusu üretebilir. Binlerce kullanıcılı üretim ortamında bu sorun, yanıt süreleri ve sunucu yükü açısından kritik hale gelir. ## N+1 sorununu anlamak N+1 sorunu, kodun bir kayıt listesini almak için bir sorgu çalıştırması (1 sorgu) ve ardından her kaydın ilişkilendirmelerine erişmek için ek bir sorgu çalıştırması (N sorgu) durumunda ortaya çıkar. "N+1" adı tam olarak bu kalıbı tanımlar: 1 başlangıç sorgusu + ilişkilendirmeler için N sorgu. Makaleler ve yazarlarıyla ilgili somut bir örnek sorunu açıklar. Optimizasyon olmadan, bir makalenin yazarına her erişim yeni bir SQL sorgusunu tetikler. ```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 ``` View içinde her `article.author` çağrısı veritabanına ek bir sorgu tetikler. ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` 100 makale için bu kod 101 SQL sorgusu üretir. Rails günlükleri tekrarlayan sorgularla sorunu açıkça ortaya koyar. ```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 ``` ## includes ile çözüm `includes` yöntemi, N+1 sorunlarını gidermek için en yaygın ve önerilen çözümdür. ActiveRecord'a ilişkilendirmeleri bir veya iki optimize edilmiş sorguda önceden yüklemesini söyler. ```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 ``` `includes` ile ActiveRecord, makale sayısından bağımsız olarak yalnızca iki sorgu çalıştırır. Birincisi tüm makaleleri, ikincisi ilgili tüm yazarları getirir. ```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, ...) ``` İç içe geçmiş ilişkilendirmeler de hash sözdizimi ile önceden yüklenebilir. Bu yaklaşım, viewlerin birden fazla ilişkilendirme düzeyine eriştiği durumlarda zorunludur. ```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 ``` > **Altın kural** > > Bir view bir döngü içinde bir ilişkilendirmeye erişiyorsa, bu ilişkilendirme controllerda `includes` ile önceden yüklenmelidir. Viewlerdeki ilişkilendirme erişim kalıpları her zaman doğrulanmalıdır. ## includes, preload ve eager_load arasındaki farklar Rails, ilişkilendirmeleri önceden yüklemek için üç yöntem sunar. Her biri, belirli kullanım durumlarına yönelik farklı bir SQL stratejisi kullanır. ### preload: ayrı sorgular `preload` yöntemi her ilişkilendirme için her zaman ayrı sorgular çalıştırır. WHERE koşulu ilişkilendirmeleri filtrelemediğinde verimli çalışır. ```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 `eager_load` yöntemi, verileri tek bir sorguda yüklemek için LEFT OUTER JOIN kullanır. İlişkilendirme sütunları üzerinde filtreleme veya sıralama yapılması gerektiğinde zorunlu hale gelir. ```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: akıllı davranış `includes` yöntemi en iyi stratejiyi otomatik olarak seçer. Varsayılan olarak `preload` kullanır, ancak bir WHERE yan tümcesi ilişkilendirmeye atıfta bulunursa `eager_load`'a geçer. ```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 ``` Aşağıdaki tablo üç yöntem arasındaki farkları özetler. | Yöntem | SQL Stratejisi | Kullanım Alanı | |--------|----------------|----------------| | `preload` | Ayrı sorgular | Basit ön yükleme, filtreleme yok | | `eager_load` | LEFT OUTER JOIN | İlişkilendirmelerde filtreleme/sıralama | | `includes` | Otomatik | Genel kullanım, önerilen varsayılan | ## Otomatik N+1 tespiti N+1 sorunlarının manuel tespiti yorucu ve hata yapmaya elverişlidir. Birkaç araç bu tespiti geliştirme ve CI ortamında otomatikleştirir. ### Bullet: gerçek zamanlı tespit Bullet gem'i SQL sorgularını gerçek zamanlı olarak analiz eder ve tespit edilen N+1 sorunlarına ilişkin uyarı verir. Ayrıca uygun düzeltmeleri de önerir. ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` Geliştirme ortamındaki yapılandırma çeşitli uyarı modlarının etkinleştirilmesine olanak tanır. ```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 ``` Bir N+1 sorunu tespit edildiğinde, Bullet önerilen çözümle birlikte açık bir mesaj görüntüler. ```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 ``` > **CI'de Bullet** > > Sürekli entegrasyonda `Bullet.raise = true` ayarının etkinleştirilmesi, bir N+1 sorunu tespit edildiğinde testlerin başarısız olmasına yol açar. Bu, performans regresyonlarını engeller. ### Prosopite: hafif alternatif Prosopite gem'i Bullet'a kıyasla daha hafif bir alternatif sunar; minimal yapılandırma ve testlerle uyumluluk sağlar. ```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 ``` ## İleri düzey optimizasyon teknikleri Temel yöntemlerin ötesinde, ActiveRecord sorgu optimizasyonunu ince ayarlamaya olanak tanıyan birkaç teknik bulunur. ### Strict Loading: varsayılan önleme Rails 6.1+ önceden yüklenmemiş bir ilişkilendirmeye erişildiğinde istisna fırlatan bir sıkı yükleme modu sağlar. Bu önleyici yaklaşım, geliştirme sırasında N+1'lerin çözülmesini zorunlu kılar. ```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 ``` Sıkı yükleme belirli bir sorgu temelinde de etkinleştirilebilir. ```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 ``` ### Kısmi veriler için Select ve Pluck Yalnızca belirli sütunlar gerektiğinde, `select` ve `pluck` veritabanından aktarılan veri miktarını azaltır. ```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 ``` ### Sayımlar için Counter Cache İlişkilendirme sayımları (`article.comments.count`) her çağrıda bir SQL sorgusu üretir. Counter cache bu sayıyı doğrudan üst tabloda saklar. ```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 ``` Migrasyon, varsayılan değerli sayım sütununu ekler. ```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 ``` Bu yapılandırmadan sonra, `article.comments_count` ek SQL sorgusu olmadan sütunu doğrudan okur. ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

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

<% end %> ``` ## En iyi uygulamalar ve kontrol listesi Sistematik bir yaklaşım, yeni geliştirmelerde N+1 sorunlarını önler ve mevcut kodu kademeli olarak düzeltir. ### Koddan önce view analizi Controller kodunu yazmadan önce, erişilen tüm ilişkilendirmeleri tespit etmek için viewi analiz etmek önerilir. Bu öngörü, atlamaları önler. ```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 ``` ### Yeniden kullanılabilir scope'lar Sık kullanılan includes'ları scope'larda merkezileştirmek bakımı kolaylaştırır ve tutarlılığı sağlar. ```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 ``` ### Önleyici kontrol listesi Bu kontrol listesi, N+1 sorunlarından kaçınmak için temel doğrulama noktalarını özetler: - Geliştirme ortamında Bullet veya Prosopite kurun ve yapılandırın - Regresyonları engellemek için CI'de Bullet.raise'i etkinleştirin - Controller yazmadan önce ilişkilendirmeleri tespit etmek için viewleri analiz edin - Varsayılan olarak `includes`, ilişkilendirmelerde filtreleme varsa `eager_load` kullanın - Sık kullanılan ön yükleme kalıpları için yeniden kullanılabilir scope'lar oluşturun - Hassas modellerde strict loading kullanın - Sık yapılan sayımlar için counter cache ekleyin - Geliştirme ortamında SQL günlüklerini düzenli olarak kontrol edin > **Aşırı ön yüklemeye dikkat** > > Çok fazla ilişkilendirmeyi önceden yüklemek belleği gereksiz yere tüketir. Yalnızca viewin gerçekten kullandığı ilişkilendirmeler önceden yüklenmelidir. Bullet gibi araçlar "unused eager loading"i de tespit eder. ## Sonuç N+1 sorguları, Rails uygulamalarında kolaylıkla önlenebilen önemli bir performans sorununu temsil eder. Otomatik tespit araçları ve geliştirme en iyi uygulamalarının birleşimi bu sorunu etkili bir şekilde ortadan kaldırır. **Önemli çıkarımlar:** - `includes` ilişkilendirmeleri önceden yükleyerek çoğu N+1 vakasını çözer - `eager_load` ilişkilendirmelerde filtreleme veya sıralama yapılırken gereklidir - Bullet ve Prosopite, geliştirme ortamında sorunları otomatik olarak tespit eder - Strict loading istisna fırlatarak N+1'leri önler - Counter cache sık yapılan sayımları optimize eder - Controller yazmadan önce viewleri analiz etmek atlamaları önler --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems