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.

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.
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.
# 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
endView içinde her article.author çağrısı veritabanına ek bir sorgu tetikler.
<!-- 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 %>100 makale için bu kod 101 SQL sorgusu üretir. Rails günlükleri tekrarlayan sorgularla sorunu açıkça ortaya koyar.
-- 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 queriesincludes 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.
# 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
endincludes 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.
-- 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.
# 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
endBir 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.
# 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.
# 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: 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.
# 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
endAş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.
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
endGeliştirme ortamındaki yapılandırma çeşitli uyarı modlarının etkinleştirilmesine olanak tanır.
# 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
endBir N+1 sorunu tespit edildiğinde, Bullet önerilen çözümle birlikte açık bir mesaj görüntüler.
# 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:5Sü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.
# 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
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.
# 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
endSıkı yükleme belirli bir sorgu temelinde de etkinleştirilebilir.
# 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
endKı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.
# 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
endSayı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.
# 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
endMigrasyon, varsayılan değerli sayım sütununu ekler.
# 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
endBu yapılandırmadan sonra, article.comments_count ek SQL sorgusu olmadan sütunu doğrudan okur.
# 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 %>Ruby on Rails mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
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.
# 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
endYeniden 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.
# 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 varsaeager_loadkullanı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
Ç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:
includesilişkilendirmeleri önceden yükleyerek çoğu N+1 vakasını çözereager_loadiliş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
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
İlgili makaleler

2026'da Rails API Modu: RESTful API, Serializasyon ve Mulakat Sorulari
Rails API-only modu ile production-ready RESTful API gelistirme: Alba ve jsonapi-serializer ile JSON serializasyonu, JWT authentication, hata yonetimi ve RSpec testleri.

Ruby on Rails Mulakat Sorulari: 2026 En Iyi 25
En cok sorulan 25 Ruby on Rails mulakat sorusu. MVC mimarisi, Active Record, migration, RSpec testi, REST API ayrintili cevaplar ve kod ornekleri ile.

Ruby on Rails 7: Reaktif Uygulamalar icin Hotwire ve Turbo
Rails 7'de Hotwire ve Turbo icin kapsamli rehber. Turbo Drive, Frames ve Streams ile JavaScript yazmadan reaktif uygulamalar gelistirme.