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.
Elimizde 10 blog yazısı var. Her blog'un bir kategorisi var. Blog listesini gösterirken kategori isimlerini de göstermek istiyoruz.
// 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?
// 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?
İ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();
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');
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:
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ç:
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');
composer require barryvdh/laravel-debugbar --dev
// ❌ 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
}
// ❌ 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;
}
// ❌ 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!
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!)