🎓 N+1 Problemi Nedir?

📅 Tarih: 2025-11-30 | 🎯 Konu: Database Performans Optimizasyonu | 👨‍🏫 Eğitim Raporu

📌 Kısaca N+1 Problemi

N+1 Problemi: Veritabanından veri çekerken, bir liste için 1 query çalıştırıp, sonra listedeki her eleman için ayrı ayrı query çalıştırarak toplam N+1 query yapmaktır.

Sonuç: 10 kayıt varsa → 11 query! 100 kayıt → 101 query! Bu, veritabanını boğar ve uygulamayı yavaşlatır.

🔍 N+1 Problemi Nasıl Oluşur?

N + 1 = 1 (Ana Query) + N (Her Kayıt İçin Ayrı Query)

📚 Örnek Senaryo: Blog Yazıları ve Kategorileri

Elimizde 10 blog yazısı var. Her blog'un bir kategorisi var. Blog listesini gösterirken kategori isimlerini de göstermek istiyoruz.

❌ YANLIŞ YAKLAŞIM (N+1 Problemi)

// 1. Query: Tüm blogları çek $blogs = Blog::all(); // 2. Her blog için döngü foreach ($blogs as $blog) { // Her blog için AYRI QUERY! echo $blog->category->title; } // TOPLAM QUERY SAYISI: // 1 (blogları çek) + 10 (her blog için kategori çek) = 11 QUERY!

Ne Oldu?

  • İlk query: Tüm blogları çekti (1 query)
  • Döngüde: Her blog için kategoriyi ayrı ayrı çekti (10 query)
  • Toplam: 11 query çalıştı!
11
Query Sayısı (10 blog için)
101
Query Sayısı (100 blog için)
1001
Query Sayısı (1000 blog için)

✅ DOĞRU YAKLAŞIM (Eager Loading)

// TEK QUERY: Blogları VE kategorileri birlikte çek $blogs = Blog::with('category')->get(); // Döngüde query çalışmaz, zaten belleğe yüklü! foreach ($blogs as $blog) { echo $blog->category->title; // Query yok! } // TOPLAM QUERY SAYISI: // 1 (blogları çek) + 1 (tüm kategorileri çek) = 2 QUERY!

Ne Değişti?

  • with('category') kullandık (Eager Loading)
  • Laravel 2 query ile her şeyi çekti
  • Döngüde query çalışmadı (veriler zaten bellekte)
2
Query Sayısı (10 blog için)
2
Query Sayısı (100 blog için)
2
Query Sayısı (1000 blog için)

❌ N+1 Problemi

  • 10 blog → 11 query
  • 100 blog → 101 query
  • 1000 blog → 1001 query
  • Yavaş ve verimisiz!

✅ Eager Loading

  • 10 blog → 2 query
  • 100 blog → 2 query
  • 1000 blog → 2 query
  • Hızlı ve verimli!

🛠️ N+1 Problemini Nasıl Çözeriz?

1 Eager Loading (with)

İlişkili verileri önceden yükle. Laravel'in en yaygın ve kolay çözümü.

// ❌ N+1 Problemi $blogs = Blog::all(); // ✅ Eager Loading $blogs = Blog::with('category')->get(); // ✅ Birden fazla ilişki $blogs = Blog::with(['category', 'author', 'tags'])->get(); // ✅ İç içe ilişkiler $blogs = Blog::with('category.parent')->get();

2 Lazy Eager Loading (load)

Veriyi çektikten sonra, ihtiyaç duyduğunda ilişkileri yükle.

// Önce blogları çek $blogs = Blog::all(); // Sonra kategorileri yükle (tek query) $blogs->load('category');

3 Global Scope ile Otomatik Eager Loading

Model'de varsayılan olarak ilişkileri yükle.

// Blog Model içinde class Blog extends Model { protected $with = ['category', 'author']; } // Artık otomatik yüklenir $blogs = Blog::all(); // category ve author otomatik gelir

⚠️ Dikkat:

  • Her zaman ihtiyaç olmayan ilişkileri ekleme
  • Sadece çok sık kullanılan ilişkileri ekle

4 Cache Kullan (Settings Örneği)

Sık kullanılan, nadiren değişen verileri cache'le.

// ❌ Her çağrıda query function setting($key) { return Setting::where('key', $key)->first()?->value; } // ✅ İlk çağrıda cache'le, sonra cache'den oku function setting($key) { return cache()->rememberForever('settings', function () { return Setting::pluck('value', 'key'); })[$key] ?? null; }

Sonuç:

❌ Cache'siz:
site_title → 1 query
site_logo → 1 query
site_name → 1 query
Toplam: 3 query
✅ Cache'li:
İlk çağrı → 1 query (tümü)
Diğer çağrılar → 0 query
Toplam: 1 query

5 Query Optimizasyonu (select, pluck)

Sadece ihtiyacın olan kolonları çek.

// ❌ Tüm kolonları çeker $blogs = Blog::all(); // ✅ Sadece ihtiyaç olan kolonlar $blogs = Blog::select('id', 'title', 'category_id')->get(); // ✅ Sadece key-value ikilisi $categories = Category::pluck('title', 'id');

🔍 N+1 Problemini Nasıl Tespit Ederiz?

1. Laravel Debugbar

  • Her sayfada kaç query çalıştığını gösterir
  • Duplicate query'leri işaretler
  • Query sürelerini gösterir
composer require barryvdh/laravel-debugbar --dev

2. Query Log (a-html.txt gibi)

  • Çalışan tüm query'leri kaydet
  • Tekrarlanan pattern'leri bul
  • Yavaş query'leri tespit et

3. Laravel Telescope

  • Request bazında query'leri gösterir
  • N+1 problemlerini otomatik tespit eder
  • Performans metriklerini takip eder

🚨 N+1 Belirtileri

  • Çok fazla query: 50+ query bir sayfada
  • Duplicate query: Aynı query defalarca çalışıyor
  • Pattern: "SELECT * FROM table WHERE id = X" şeklinde 10+ query
  • Yavaşlık: Sayfa 5+ saniyede yükleniyor

🎯 Gerçek Hayat Örnekleri

Örnek 1: Blog + Kategori + Yazar

// ❌ N+1 Problemi (1 + 10 + 10 = 21 query) $blogs = Blog::all(); foreach ($blogs as $blog) { echo $blog->category->title; // +10 query echo $blog->author->name; // +10 query }
// ✅ Eager Loading (1 + 1 + 1 = 3 query) $blogs = Blog::with(['category', 'author'])->get(); foreach ($blogs as $blog) { echo $blog->category->title; // 0 query echo $blog->author->name; // 0 query }

Örnek 2: Ürünler + Marka + Kategori

// ❌ N+1 Problemi (1 + 50 + 50 = 101 query) $products = Product::all(); foreach ($products as $product) { echo $product->brand->name; echo $product->category->title; }
// ✅ Eager Loading (1 + 1 + 1 = 3 query) $products = Product::with(['brand', 'category'])->get(); foreach ($products as $product) { echo $product->brand->name; echo $product->category->title; }

Örnek 3: Settings (Bizim Sorunumuz!)

// ❌ N+1 Problemi (Her çağrıda 1 query) setting('site_title'); // 1 query setting('site_logo'); // 1 query setting('site_name'); // 1 query setting('site_title'); // 1 query (duplicate!) setting('site_logo'); // 1 query (duplicate!) // Toplam: 5 query (2 duplicate)
// ✅ Cache Çözümü (İlk çağrıda 1 query, sonrası 0) function setting($key) { return cache()->rememberForever('all_settings', function () { return Setting::with('values') ->pluck('value', 'key'); })[$key] ?? null; } setting('site_title'); // 1 query (ilk çağrı, tümünü cache'ler) setting('site_logo'); // 0 query (cache'den) setting('site_name'); // 0 query (cache'den) setting('site_title'); // 0 query (cache'den) setting('site_logo'); // 0 query (cache'den) // Toplam: 1 query, 0 duplicate!

📊 Performans Karşılaştırması

1001
N+1 (1000 kayıt)
2
Eager Loading (1000 kayıt)
500x
Hızlanma Oranı

❌ N+1 Problemi (1000 kayıt)

  • Query Sayısı: 1001
  • Süre: ~10-15 saniye
  • Database Yük: ÇOK YÜKSEK
  • Bellek: NORMAL

✅ Eager Loading (1000 kayıt)

  • Query Sayısı: 2
  • Süre: ~20-50ms
  • Database Yük: ÇOK DÜŞÜK
  • Bellek: BİRAZ FAZLA

✅ Sonuç ve Tavsiyeler

🎯 Ana Kurallar

  • 1. Her zaman Eager Loading kullan: with() ile ilişkileri önceden yükle
  • 2. Debugbar kullan: Query sayısını sürekli kontrol et
  • 3. Cache kullan: Sık okunan, nadiren değişen veriler için
  • 4. Sadece gerekeni çek: select() ile kolonları sınırla
  • 5. Test et: Her değişiklikten sonra query sayısını kontrol et

📚 Kaynak ve Araçlar

  • Laravel Debugbar (query izleme)
  • Laravel Telescope (performans izleme)
  • a-html.txt debug çıktıları (kendi sistemimiz)
  • Redis cache (hızlı veri saklama)

⚠️ Dikkat Edilmesi Gerekenler

  • Aşırı Eager Loading: Gereksiz ilişkileri yükleme (bellek şişer)
  • Cache süresi: Çok uzun cache, güncel olmayan veri demek
  • Global with: Model'de $with kullanırken dikkatli ol
  • Development ortamı: Debugbar'ı production'da kapatmayı unutma

🎓 Özet

N+1 Problemi Nedir?

Liste için 1 query + listedeki her eleman için ayrı query = N+1 query

Çözüm: Eager Loading (with), Cache, Query Optimizasyonu

Sonuç: 1001 query → 2 query (500x hızlanma!)