TL;DR — N+1 sorgu problemi, listeler üzerinde performansı ciddi şekilde yok eder.
Bunu hedefli eager loading ile (with()), SQL tarafında toplamalar
(withCount,withSum) ve temel indeksleme ile çözebilirsiniz. Debugbar/Telescope
ya daEXPLAIN ANALYZEile ölçüm yapın. Bu yazıda tam olarak nasıl yapacağınızı göreceksiniz.
Semptom: 1 + N Sorgu
Semptom: 1 + N Sorgu
Bir liste (siparişler, gönderiler, ürünler) render ediyorsunuz. Her satır için ilişkili bir modele (->customer, ->items, ->author) erişiyorsunuz ve Eloquent, her satır için bir ekstra sorgu çalıştırıyor.
Bu durumda 1 sorgu listeyi almak için + N sorgu her satırın ilişkisi için:
20 satırda, sayımlar ve iç içe ilişkiler eklendiğinde genellikle 120 sorguya ulaşıyorsunuz.
Önce: klasik N+1
Önce: klasik N+1
// Controller / Repository
$orders = Order::latest()->take(20)->get();
foreach ($orders as $o) {
echo $o->customer->name; // her satır için ekstra sorgu
echo $o->items()->count(); // her satır için ekstra sorgu
}
Bu “çalışır”, ama pahalıdır ve hacimle daha kötü hale gelir.
Çözüm: Bir Kez Yükle, Çok Kullan
Çözüm: Bir Kez Yükle, Çok Kullan
Sonra: eager loading + SQL toplamları
Sonra: eager loading + SQL toplamları
$orders = Order::with(['customer:id,name']) // ilişkileri tek seferde getir
->withCount('items') // SQL’de sayıyı hesapla
->latest()
->take(20)
->get();
foreach ($orders as $o) {
echo $o->customer->name; // zaten bellekte
echo $o->items_count; // veritabanı tarafından önceden hesaplandı
}
Sonuç: tipik kurulumlarda toplam 8-10 sorgu yerine 100+ sorgu.
N+1’i Hızla Tespit Etme
N+1’i Hızla Tespit Etme
- Laravel Debugbar veya Telescope: sayfadaki toplam sorgu sayısına bakın.
- Yerel ortamda katı modu etkinleştirin ve tembel yüklemeleri görün:
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot()
{
if (!app()->isProduction()) {
Model::preventLazyLoading();
}
}
Bu, bir tembel yükleme gerçekleştiğinde geliştirme sırasında hata verecek ve sizi tam satıra yönlendirecektir.
Eager Loading’i Doğru Kullanma
Eager Loading’i Doğru Kullanma
1) Sadece ihtiyacınız olanı hedefleyin
1) Sadece ihtiyacınız olanı hedefleyin
İlişkileri yükleyin ve sütunları kısıtlayın böylece yükleme boyutlarını küçük tutun:
$posts = Post::with([
'author:id,name',
'tags:id,name',
])->get(['id','title','author_id']);
2) Gerektiğinde derinlemesine gidin
2) Gerektiğinde derinlemesine gidin
$posts = Post::with(['comments.author'])->get();
3) Mevcut koleksiyonlar üzerinde
3) Mevcut koleksiyonlar üzerinde
Zaten modeller yüklüyse, her şeyi yeniden yüklemeden ilişkileri ekleyin:
$orders->load(['customer', 'items']);
$orders->loadCount('items');
4) Tekrarlanan eager yüklemeleri önleyin
4) Tekrarlanan eager yüklemeleri önleyin
$orders->loadMissing('customer');
Kullanmanız Gereken Toplamlar (Hepsi SQL’de)
Kullanmanız Gereken Toplamlar (Hepsi SQL’de)
withCount('items')withSum('items as total_price', 'price')withAvg('items', 'price')
Örnek:
$orders = Order::with('customer:id,name')
->withCount('items')
->withSum('items as total_price', 'price')
->latest()
->take(20)
->get();
Şimdi Blade şununla temiz bir şekilde okunabiliyor:
{{ $order->customer->name }}
{{ $order->items_count }}
{{ number_format($order->total_price, 2) }}
Veritabanı Tarafı: Planlayıcıya Bir Şans Verin
Veritabanı Tarafı: Planlayıcıya Bir Şans Verin
Her ne kadar mükemmel eager loading olsa da, veritabanınızda indeks yoksa sorun yaşayabilirsiniz.
Yabancı Anahtarlarınızı İndeksleyin
Yabancı Anahtarlarınızı İndeksleyin
PostgreSQL
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_items_order_id ON items(order_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_customer_id ON orders(customer_id);
MySQL
ALTER TABLE items ADD INDEX idx_items_order_id (order_id);
ALTER TABLE orders ADD INDEX idx_orders_customer_id (customer_id);
EXPLAIN ANALYZE ile Ölçüm Yapın
EXPLAIN ANALYZE ile Ölçüm YapınPlanlayıcının mantıklı işlemler kullandığını kontrol edin (Index Scan, Hash Aggregate).
Büyük tablolarda tam taramalar görüyorsanız, muhtemelen indeksler veya daha iyi filtrelere ihtiyacınız vardır.
Gerçek Dünya Kontrol Listesi (kopyala-yapıştır)
Gerçek Dünya Kontrol Listesi (kopyala-yapıştır)
- [ ] İlişkili verileri kullanan her liste için
with()kullanın - [ ] Satır başına
->count()yerinewithCount()kullanın - [ ] Satır başına toplamları
withSum()ile değiştirin - [ ] Sütunları kısıtlayın:
relation:id,needed_columns - [ ] Yabancı anahtar sütunlarında indeksler ekleyin
- [ ] Yerel ortamda
Model::preventLazyLoading()özelliğini etkinleştirin - [ ] Debugbar/Telescope ile sorgu sayısını doğrulayın
- [ ] Planları
EXPLAIN ANALYZEile kontrol edin
Bonus: Temel Yüklemeye Hâlâ İhtiyacınız Olduğunda
Bonus: Temel Yüklemeye Hâlâ İhtiyacınız Olduğunda
Bazen gerçekten ad-hoc ilişki erişimine ihtiyaç duyarsınız. Bunu açık ve nadir; yapın veya küçük verileri yükleyin, böylece zararı en aza indirin.
Order::whereKey($id)->first()->load('customer');
İpucu: verilerin sık kullanılması durumunda Cache::remember() ile eşleştirin.
Paylaşabileceğiniz Minimal “Önce/Sonra” Görseli
Paylaşabileceğiniz Minimal “Önce/Sonra” Görseli
Ekibiniz veya sosyal medya paylaşımlarınız için bu tek ekran karşılaştırmasını kullanın:
- // ÖNCE — N+1: 1 liste sorgusu + N satır başına ilişkiler
- $orders = Order::latest()->take(20)->get();
- foreach ($orders as $o) {
- echo $o->customer->name; // her satır için sorgu
- echo $o->items()->count(); // her satır için sorgu
- }
+ // SONRA — eager + SQL toplamları: ~8 toplam sorgu
+ $orders = Order::with(['customer:id,name'])
+ ->withCount('items')
+ ->latest()->take(20)->get();
+ foreach ($orders as $o) {
+ echo $o->customer->name; // zaten yüklenmiş
+ echo $o->items_count; // SQL'de hesaplandı
+ }
Yaygın Tuzaklar ve Bunlardan Kaçınma Yöntemleri
Yaygın Tuzaklar ve Bunlardan Kaçınma Yöntemleri
- Çok fazla yüklemek:
with('bigRelation')kolon limitleri olmadan bellek kullanımını artırabilir.with(['bigRelation' => fn ($q) => $q->select('id','fk_id','name')])kullanın. - İç içe N+1: İlk ilişkiyi düzeltin… ve ikincisini unutun (
comments.author). Her ikisini de yükleyin. - Koleksiyona göre saymak:
->items->count()(koleksiyon) kullanmaktan kaçının,withCount('items')mevcutken. - Önbellek geçersiz kılma: Önbellek kullanıyorsanız, her zaman filtrelere göre anahtar oluşturun (kullanıcı, dükkan, yerel ayar…).
Bunun Önemi (Benchmark’ların Ötesinde)
Bunun Önemi (Benchmark’ların Ötesinde)
- Daha hızlı sayfalar → daha yüksek dönüşüm (arama, rezervasyon, satın alma).
- Daha düşük DB baskısı → daha küçük örnekler, daha iyi istikrar.
- Daha temiz kod → regresyon olmadan geliştirmek daha kolay.
Kaynak: Orijinal Makale
- Semptom: 1 + N Sorgu
- Çözüm: Bir Kez Yükle, Çok Kullan
- N+1’i Hızla Tespit Etme
- Eager Loading’i Doğru Kullanma
- 1) Sadece ihtiyacınız olanı hedefleyin
- 2) Gerektiğinde derinlemesine gidin
- 3) Mevcut koleksiyonlar üzerinde
- 4) Tekrarlanan eager yüklemeleri önleyin
- Kullanmanız Gereken Toplamlar (Hepsi SQL’de)
- Veritabanı Tarafı: Planlayıcıya Bir Şans Verin
- Gerçek Dünya Kontrol Listesi (kopyala-yapıştır)
- Bonus: Temel Yüklemeye Hâlâ İhtiyacınız Olduğunda
- Paylaşabileceğiniz Minimal “Önce/Sonra” Görseli
- Yaygın Tuzaklar ve Bunlardan Kaçınma Yöntemleri
- Bunun Önemi (Benchmark’ların Ötesinde)


