Laravel N+1 sorgu problemi, Eloquent ile çalışan hemen her projenin er ya da geç karşılaştığı sinsi bir performans sorunudur. Kod tertemiz görünür, testler geçer, sayfa lokalde hızlı açılır; ama veritabanında birkaç bin satır birikince aynı sayfa aniden yavaşlar. Sebep çoğu zaman tek bir şeydir: döngü içinde, farkında olmadan veritabanına onlarca hatta yüzlerce ek sorgu göndermek. Bu yazıda problemin neden oluştuğunu, query log ile gizli sorguları nasıl yakalayacağını ve eager loading ile nasıl kalıcı olarak çözeceğini adım adım anlatıyorum.
N+1 problemi tam olarak nedir?
İsim aslında her şeyi anlatıyor. Bir ana sorgu (1) çalışır ve N tane kayıt döner; sonra bu N kaydın her biri için ilişkili veriyi çekmek üzere birer sorgu daha (N) çalışır. Toplam 1 + N sorgu. 20 yazı listeliyorsan ve her yazının yazarını gösteriyorsan, 1 sorgu yazılar için, 20 sorgu da yazarlar için gider: 21 sorgu.
Klasik örneğe bakalım. Bir blog listesinde her yazının yazar adını göstermek istiyoruz:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
İlk satır tek bir select * from posts sorgusu üretir. Ama döngü içindeki $post->author ifadesi, her yazı için ayrı bir select * from users where id = ? sorgusu tetikler. Eloquent ilişkiyi lazy loading (tembel yükleme) ile, yani ilk erişildiği anda yükler. Bu yüzden 100 yazı = 101 sorgu olur ve bunu çıktıya bakarak asla fark etmezsin.
Query log ile gizli sorguları yakalamak
Problemi çözmenin ilk adımı görmektir. Laravel'in DB facade'ı çalışan tüm sorguları kaydedebilir. Geçici olarak bir route veya controller içinde şöyle açabilirsin:
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
$posts = Post::all();
foreach ($posts as $post) {
$post->author->name;
}
dd(DB::getQueryLog());
Çıktıda 101 satır görürsen, suçlu yakalanmış demektir. Aynı SQL'in where id = ? ile defalarca tekrar etmesi N+1'in en net imzasıdır.
Daha kalıcı bir yöntem, bir service provider içinde her sorguyu loglamaktır:
DB::listen(function ($query) {
logger()->info($query->sql, $query->bindings);
});
Bunu AppServiceProvider'ın boot metoduna koyup storage/logs/laravel.log dosyasını izlersen, tek bir istekte kaç sorgu çalıştığını net görürsün. Geliştirme ortamında Laravel Debugbar ya da Telescope gibi araçlar da bu sayıyı her sayfanın altında otomatik gösterir; tekrar eden sorguları gözle taramak yerine sayacın yükseldiğini fark edersin.
Çözüm: eager loading ile with()
Çözüm şaşırtıcı derecede basit. İlişkiyi döngüde tek tek yüklemek yerine, ana sorgu çalışırken Eloquent'e "bu ilişkiyi de baştan getir" demen yeterli. Bunu with() ile yaparsın:
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
Şimdi Eloquent yalnızca iki sorgu çalıştırır: biri yazılar için, biri de tüm yazıların yazarlarını tek seferde getiren select * from users where id in (1, 2, 3, ...) sorgusu. 100 yazı için 101 sorgu yerine 2 sorgu. İşte N+1'in 1 + 1'e dönüşmesi budur.
Birden fazla ilişkiyi ve iç içe ilişkileri de aynı anda yükleyebilirsin:
$posts = Post::with(['author', 'comments.user', 'tags'])->get();
Burada comments.user nokta sözdizimi, her yorumun kullanıcısını da yükler; böylece yorumları gezerken yeni bir N+1 oluşmaz. Sadece belirli sütunları çekerek sorguyu hafifletmek istersen:
$posts = Post::with('author:id,name')->get();
Dikkat: alt seçimde ilişkinin foreign key'i olan id sütununu mutlaka dahil et, yoksa Eloquent kayıtları eşleştiremez ve ilişki null döner.
Diğer faydalı teknikler
- loadMissing(): Modelleri zaten elde ettiysen ve ilişkinin yüklenip yüklenmediğinden emin değilsen, gereksiz tekrar sorgu atmadan eksikleri tamamlar:
$posts->loadMissing('author'). - withCount(): Sadece ilişkili kayıt sayısını gösteriyorsan, tüm satırları çekmek yerine sayıyı tek sorguda al:
Post::withCount('comments')->get(). Sonuç$post->comments_countolarak gelir. - Otomatik koruma: Laravel, lazy loading'i komple yasaklamana izin verir.
AppServiceProvideriçineModel::preventLazyLoading(! app()->isProduction())eklersen, eager load edilmemiş bir ilişkiye eriştiğinde geliştirme ortamında istisna fırlatır. Böylece N+1'i daha kod yazarken yakalarsın.
Eager loading güçlüdür ama her şeye with() ekleme refleksine kapılma. Bir ilişkiyi sayfada gerçekten kullanmıyorsan onu yüklemek ters tepki yapıp belleği şişirir. Kural basit: döngü içinde erişilen her ilişki eager load edilmeli; erişilmeyen hiçbir ilişki edilmemeli.
Sık Sorulan Sorular
N+1 problemi her zaman gerçek bir sorun mudur?
Küçük veri kümelerinde fark edilmez; 5 kayıt için 6 sorgu kimseyi yormaz. Ama veri büyüdükçe sorgu sayısı doğrusal artar ve yüzlerce sorgu hem veritabanını hem yanıt süresini boğar. En kötüsü, lokalde fark etmeyip canlıda patlamasıdır; bu yüzden erkenden ölçmek doğru yaklaşımdır.
with() ile load() arasındaki fark nedir?
with() sorgu kurulurken, yani modeller henüz çekilmeden ilişkiyi planlar. load() ise zaten elinde olan bir koleksiyona sonradan ilişki ekler. İkisi de aynı toplu sorgu mantığını kullanır; hangisini seçeceğin modeli ne zaman elde ettiğine bağlıdır.
Eager loading'i nasıl test ortamında zorunlu kılarım?
Model::preventLazyLoading() çağrısını yalnızca production dışında etkinleştir. Böylece test ve geliştirme sırasında eager load unutulan her ilişki istisna fırlatır, canlıda ise sessizce normal davranır.
Laravel uygulamanız beklenenden yavaş mı? Sıklıkla suçlu gizli N+1 sorgularıdır. Projenizi birlikte profilleyip sorgu sayısını düşürebiliriz; benimle iletişime geçin.