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.

Ruby on Rails'te ActiveRecord ile N+1 Sorgu Sorunlarını Çözme

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
<!-- 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.

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

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

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#ruby on rails
#activerecord
#performance
#n+1 queries
#sql optimization

Paylaş

İlgili makaleler