# 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| %>