Bir yönetim panelinde 20 siparişi listeleyen ekranınız ilk bakışta çok hızlı açılabilir. Liste büyüdüğünde veya her satıra müşteri adı, ürün sayısı ve durum bilgisi eklendiğinde aynı sayfanın yüzlerce sorgu çalıştırdığını fark edebilirsiniz. Bu durumun sık karşılaşılan nedenlerinden biri, ilişkili kayıtların her satır için ayrı ayrı yüklenmesidir: N+1 sorgu sorunu.

N+1 tam olarak nedir?

Önce siparişleri alan bir sorgu çalıştığını düşünün. Ardından döngü içinde her siparişin müşterisine erişildiğinde, müşteri ilişkisi henüz yüklenmemişse Eloquent her sipariş için ek bir sorgu yapabilir. 20 sipariş için bir liste sorgusuna 20 müşteri sorgusu eklenmesi, basit bir N+1 örneğidir. Gerçek sayı mevcut ilişki tanımlarına ve uygulamanın neler okuduğuna göre değişir.

PHP
$orders = Order::query()->latest()->limit(20)->get();

foreach ($orders as $order) {
    echo $order->customer?->name;
}

Bu örnekte customer bir belongsTo ilişkisi ve ilişki önceden yüklenmemiştir. Yalnızca kodun döngüde bulunması değil, döngü içinde veritabanına erişen ilişkinin tetiklenmesi soruna yol açar. Sayfanın Blade şablonunda, bir API kaynağında veya model erişimcisinde de aynı durum gizlenebilir.

Önce ölçün, ardından eager loading kullanın

Geliştirme ortamında sorgu günlüğü, hata ayıklama araçları veya veritabanı izleme araçlarıyla ilgili isteğin sorgu sayısına ve süresine bakın. Üretimde ayrıntılı sorgu kayıtlarının hassas veri içerebileceğini ve ek yük oluşturabileceğini unutmayın. Sorunu gerçekçi sayıda kayıtla tekrar etmek önemlidir; tek satırlık test verisinde N+1 kolayca gözden kaçar.

İlişkiyi listeyle birlikte önceden yüklemek için with kullanılabilir:

PHP
$orders = Order::query()
    ->with('customer')
    ->latest()
    ->limit(20)
    ->get();

Eloquent tipik olarak siparişleri alır, ardından bu siparişlerle ilişkili müşterileri toplu bir sorguyla yükler. Böylece her sipariş için ayrı sorgu yapma ihtiyacı ortadan kalkar. Sorgu sayısının düşmesi genellikle faydalıdır; yine de ağ, veritabanı planı ve veri miktarı nedeniyle tek başına performans garantisi değildir.

Sadece ihtiyacınız olan veriyi alın

Listede yalnızca ürün adedi gösteriliyorsa bütün ürün modellerini yüklemek gereksiz olabilir. Böyle bir durumda withCount('items'), ilişki verisini koleksiyon olarak çekmeden sayıyı items_count alanında sunabilir. İlişkili kayıtlarda belirli sütunları seçerken ilişkiyi kurmak için gereken anahtar sütunları da seçin; aksi hâlde Eloquent modelleri doğru eşleştiremeyebilir.

PHP
$orders = Order::query()
    ->with('customer:id,name')
    ->withCount('items')
    ->latest()
    ->paginate(20);

Burada orders tablosundaki customer_id anahtarının ve ilişkili müşteri tablosundaki id alanının seçilebilir durumda olması gerekir. Bu örnek varsayılan Eloquent anahtar adlarına göre verilmiştir; projenizde özel anahtarlar kullanıyorsanız seçimi ilişki tanımınıza göre uyarlayın.

Her ilişkiyi yüklemek de çözüm değil

Bir sayfada kullanılmayan büyük ilişkileri de with listesine eklemek bellek tüketimini ve aktarılan veri miktarını artırabilir. Özellikle ürünlerin açıklamaları, büyük JSON alanları veya iç içe ilişkiler söz konusuysa sorgu sayısı düşerken toplam süre artabilir. Önce ihtiyaç duyulan alanları belirleyin, ardından hem sorgu süresini hem yüklenen kayıt sayısını karşılaştırın.

Geliştirme sırasında Eloquent’in lazy loading engelleme özelliği beklenmeyen ilişki yüklemelerini görünür kılmak için kullanılabilir. Bunun üretimde istisnaya dönüşüp dönüşmeyeceği yapılandırmanıza bağlı olduğundan uygulamaya almadan önce mevcut davranışı ve testleri kontrol edin.

Son kontrol için aynı sayfayı boş liste, tek kayıt ve yüzlerce kayıt senaryolarıyla deneyin. Sayfalama, ilişki sayımları ve nested ilişkiler ekledikten sonra sorgu sayısını tekrar gözlemleyin. Amacımız sıfır sorgu değil; her sorgunun neden yapıldığını ve büyüyen veriyle nasıl davranacağını bilmektir.

Kaynak: Laravel Eloquent ilişkileri ve eager loading