Merhaba arkadaşlar, bugün başımı çok ağrıtan bir sorundan ve bulduğum temiz çözümden bahsedeceğim. Laravel'de büyük veri setleriyle uğraşırken, N+1 sorgu problemini çözmek için `with()` kullanmak ilk refleksimiz. Ama işler biraz büyüyünce, bu sefer de memory limit hatasıyla karşılaşabiliyoruz. Tüm kullanıcıları ve onların yüzlerce siparişini tek seferde çekmeye kalktığınızda, sunucunuz "Allah korusun" diyebilir. İşte benim kullandığım, iki dünyayı bir araya getiren kombinasyon.
Karşılaştığım Sorun
Bir raporlama sisteminde, aktif tüm kullanıcıların (diyelim 50.000) ve bu kullanıcılara ait son bir yıllık siparişlerin toplam tutarını hesaplamam gerekiyordu. Klasik `User::with('orders')->get()` yaklaşımı, önce tüm kullanıcıları, sonra da ilişkili tüm siparişleri (milyonlarca satır) aynı anda memory'e yükledi. Sonuç? `Allowed memory size of X bytes exhausted`. `chunk()` metodunu denedim ama bu sefer de her chunk için ayrı ayrı ilişkileri yükleme (N+1) veya tüm ilişkileri tekrar tekrar yükleme sorunu ortaya çıkıyordu.
Çözüm Fikri: Cursor ve Lazy Eager Loading
Aklıma iki güzel özelliği birleştirmek geldi: `cursor()` ve `loadMissing()`. `cursor()`, bir Generator döndürür ve tüm sonucu bir kerede değil, tek bir satır (model instance'ı) halinde memory'e yükler. `loadMissing()` ise, Lazy Eager Loading yaparak, sadece ihtiyaç duyulan modeller için ilişkileri yükler. İkisini birleştirince, hem memory dostu hem de N+1'den arınmış bir akış elde ettim.
Uygulama ve Kod Örnekleri
İşte benim uyguladığım temiz çözümün kodu:
Burada kritik noktalar:
- `cursor()` sayesinde tek seferde sadece bir User modeli memory'de.
- `loadMissing()` ile o anki kullanıcının sadece gerekli siparişleri (son bir yıl) yükleniyor. Bu, `with()` ile tüm siparişleri çekmekten çok daha verimli.
- `unset($user->orders);` satırı, işi biten ilişkili koleksiyonu memory'den manuel olarak boşaltıyor. Bu küçük dokunuş, uzun döngülerde memory sızıntısını engellemek için altın değerinde.
Performans ve Sonuç
Bu yöntemi uygulamadan önce, 50k kullanıcı için script maksimum memory limitine (512MB) çarpıp ölüyordu. Bu kombinasyonu uyguladıktan sonra, memory kullanımı sabit bir seviyede (yaklaşık 10-15MB civarında) kaldı ve işlem sorunsuz tamamlandı. Sorgu sayısı da optimize oldu çünkü her kullanıcı için ayrı sipariş sorgusu atılmadı (`loadMissing` bunu akıllıca grupluyor).
Peki siz Laravel'de büyük veri işlerken benzer sorunlarla karşılaştınız mı? `chunk()` ve `cursor` arasında tercihiniz ne? Ya da farklı bir memory optimizasyon taktiğiniz var mı? Yorumlarda deneyimlerinizi paylaşın, tartışalım!
Bir raporlama sisteminde, aktif tüm kullanıcıların (diyelim 50.000) ve bu kullanıcılara ait son bir yıllık siparişlerin toplam tutarını hesaplamam gerekiyordu. Klasik `User::with('orders')->get()` yaklaşımı, önce tüm kullanıcıları, sonra da ilişkili tüm siparişleri (milyonlarca satır) aynı anda memory'e yükledi. Sonuç? `Allowed memory size of X bytes exhausted`. `chunk()` metodunu denedim ama bu sefer de her chunk için ayrı ayrı ilişkileri yükleme (N+1) veya tüm ilişkileri tekrar tekrar yükleme sorunu ortaya çıkıyordu.
Aklıma iki güzel özelliği birleştirmek geldi: `cursor()` ve `loadMissing()`. `cursor()`, bir Generator döndürür ve tüm sonucu bir kerede değil, tek bir satır (model instance'ı) halinde memory'e yükler. `loadMissing()` ise, Lazy Eager Loading yaparak, sadece ihtiyaç duyulan modeller için ilişkileri yükler. İkisini birleştirince, hem memory dostu hem de N+1'den arınmış bir akış elde ettim.
İşte benim uyguladığım temiz çözümün kodu:
PHP:
// Raporu oluşturacak metodumuz
public function generateUserOrderReport()
{
$reportData = [];
// 1. Tüm kullanıcıları cursor ile tek tek al
foreach (User::where('active', true)->cursor() as $user) {
// 2. Bu kullanıcı için siparişleri LAZY olarak yükle (N+1'i önler)
$user->loadMissing(['orders' => function ($query) {
$query->where('created_at', '>=', now()->subYear());
}]);
// 3. Sipariş toplamını hesapla
$totalAmount = $user->orders->sum('amount');
// 4. Rapor dizisine ekle (gerekiyorsa)
$reportData[] = [
'user_id' => $user->id,
'name' => $user->name,
'total_order_amount' => $totalAmount
];
// 5. ÖNEMLİ: İlişkili koleksiyonu temizle (memory'den at)
unset($user->orders);
}
return $reportData;
}
Burada kritik noktalar:
- `cursor()` sayesinde tek seferde sadece bir User modeli memory'de.
- `loadMissing()` ile o anki kullanıcının sadece gerekli siparişleri (son bir yıl) yükleniyor. Bu, `with()` ile tüm siparişleri çekmekten çok daha verimli.
- `unset($user->orders);` satırı, işi biten ilişkili koleksiyonu memory'den manuel olarak boşaltıyor. Bu küçük dokunuş, uzun döngülerde memory sızıntısını engellemek için altın değerinde.
Bu yöntemi uygulamadan önce, 50k kullanıcı için script maksimum memory limitine (512MB) çarpıp ölüyordu. Bu kombinasyonu uyguladıktan sonra, memory kullanımı sabit bir seviyede (yaklaşık 10-15MB civarında) kaldı ve işlem sorunsuz tamamlandı. Sorgu sayısı da optimize oldu çünkü her kullanıcı için ayrı sipariş sorgusu atılmadı (`loadMissing` bunu akıllıca grupluyor).
Peki siz Laravel'de büyük veri işlerken benzer sorunlarla karşılaştınız mı? `chunk()` ve `cursor` arasında tercihiniz ne? Ya da farklı bir memory optimizasyon taktiğiniz var mı? Yorumlarda deneyimlerinizi paylaşın, tartışalım!