Laravel Eloquent İleri Düzey Optimizasyon Teknikleri
Eloquent, Laravel'in en çok kullanılan bileşeni olduğu kadar, performans sorunlarının da en sık görüldüğü yeridir. Bu rehber, orta seviye Laravel geliştiricilerinin sorgu katmanını öngörülebilir biçimde hızlandırmasına odaklanır.
Önkoşullar ve Kapsam
- Laravel 10 veya 11 varsayılmıştır. Buradaki API'lerin çoğu Laravel 8'den itibaren mevcuttur; sürüm farkları ilgili başlıklarda belirtilir.
- Eloquent modelleri, ilişkiler ve query builder hakkında temel bilgi beklenir.
- Örneklerdeki modeller (
User,Post,Comment) tipik bir blog şemasını temsil eder. Şemanıza uyarlamanız gerekir. - Sürüm numarası, fiyat veya benchmark değeri verilmemektedir; ölçümü kendi ortamınızda yapmalısınız.
1. Sorgu Sayısını Ölçün, Tahmin Etmeyin
Optimizasyona başlamadan önce darboğazı ölçün. Laravel, sorguları gözlemlemek için birkaç yerleşik yol sunar.
DB::listen ile sorgu günlüğü:
use Illuminate\Support\Facades\DB;
DB::listen(function ($query) {
logger()->info($query->sql, [
'bindings' => $query->bindings,
'time_ms' => $query->time,
]);
});
Geliştirme ortamında Laravel Debugbar (barryvdh/laravel-debugbar) N+1 sorunlarını gözle görülür hale getirir. Üretimde kapalı tutun.
Beklenen davranış: Bir liste sayfası açtığınızda, kayıt sayısıyla doğru orantılı artan sorgu sayısı görüyorsanız N+1 problemi vardır.
2. N+1 Problemini İlişki Yüklemesiyle Çözün
En yaygın performans hatası, döngü içinde ilişki erişimidir.
Sorunlu:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // her post için ayrı sorgu
}
Çözüm — eager loading:
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // tek ek sorgu
}
İç içe ilişkiler için nokta notasyonu kullanın:
$posts = Post::with('user.profile', 'comments.author')->get();
Koşullu eager loading:
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true)->latest();
}])->get();
Yalnızca belirli sütunlara ihtiyacınız varsa, ilişki yüklemesinde sütun seçin. Dikkat: birincil anahtar ve yabancı anahtar sütunlarını dahil etmezseniz ilişki boş döner.
Post::with('user:id,name')->get();
3. Yalnızca İhtiyaç Duyduğunuz Sütunları Seçin
SELECT * gereksiz veri taşır ve indeks kullanımını engelleyebilir.
Post::select('id', 'title', 'created_at')->get();
Bununla birlikte select kullanırken model erişimcilerinin (accessor) ihtiyaç duyduğu sütunları da eklemeyi unutmayın; aksi halde null döner.
4. chunk, cursor ve Bellek Yönetimi
Büyük tabloları get() ile belleğe çekmek uygulamayı çökertebilir.
chunk — sayfa sayfa işleme:
Post::where('published', true)
->chunkById(500, function ($posts) {
foreach ($posts as $post) {
// işle
}
});
chunk yerine chunkById tercih edin; kayıtları güncellerken atlama/sıçrama riskini azaltır.
cursor — tek sorgu, lazy iterasyon:
foreach (Post::where('published', true)->cursor() as $post) {
// işle
}
cursor daha az bellek kullanır ama tek bir sorgu açar; uzun süren işlemlerde veritabanı bağlantısını meşgul eder.
lazy — ilişki yüklemeli parçalı iterasyon:
Post::with('user')->lazy(500)->each(function ($post) {
// işle
});
5. withCount, withSum ve Alt Sorgular
İlişkili kayıt sayısını almak için tüm ilişkiyi yüklemek yerine sayım kullanın.
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Koşullu sayım:
Post::withCount(['comments as approved_comments_count' => function ($query) {
$query->where('approved', true);
}])->get();
Toplam için withSum, ortalama için withAvg, en büyük/küçük değer için withMax / withMin kullanılabilir.
6. whereHas ve withWhereHas
İlişkiye göre filtreleme yaparken whereHas alt sorgu üretir:
Post::whereHas('comments', function ($query) {
$query->where('approved', true);
})->get();
Hem filtreleyip hem ilişkiyi yüklemek istiyorsanız:
Post::withWhereHas('comments', function ($query) {
$query->where('approved', true);
})->get();
whereHas alt sorguları, yüksek hacimli tablolarda indekslenmemiş sütunlarda yavaşlar. Yabancı anahtar ve filtre sütunlarının indeksli olduğundan emin olun.
7. İndeksler ve explain
Eloquent seviyesindeki iyileştirmeler, veritabanı indeksleri yoksa sınırlı fayda sağlar. Sorgularınızı EXPLAIN ile inceleyin:
DB::table('posts')
->where('user_id', 1)
->where('published', true)
->explain();
Bileşik indekslerde sütun sırası önemlidir: sorgunuzda en sık filtrelenen sütunlar önce gelmelidir. Yabancı anahtarlar (user_id, post_id) çoğu veritabanında otomatik indekslenir; ancak filtreleme yaptığınız published, approved gibi sütunlar için ayrı indeks gerekir.
8. Toplu İşlemler
Tek tek save() çağırmak yerine toplu yazma ve güncelleme kullanın.
// Toplu ekleme
Post::insert([
['title' => 'A', 'user_id' => 1, 'created_at' => now(), 'updated_at' => now()],
['title' => 'B', 'user_id' => 1, 'created_at' => now(), 'updated_at' => now()],
]);
// Toplu güncelleme
Post::where('user_id', 1)->update(['published' => true]);
Dikkat: insert ve toplu update çağrıları model olaylarını (events), accessor/mutator'ları ve timestamps otomatiğini tetiklemez. created_at/updated_at değerlerini elle eklemeniz gerekir.
9. upsert ile Ekleme-Güncelleme
Aynı kaydı ekleyip güncelleyen mantık için upsert tek sorguda çalışır:
Post::upsert(
[
['id' => 1, 'title' => 'Güncel', 'slug' => 'guncel'],
['id' => 2, 'title' => 'Yeni', 'slug' => 'yeni'],
],
['id'], // benzersiz anahtar
['title'] // güncellenecek sütunlar
);
İkinci argüman benzersiz anahtar(lar), üçüncü argüman çakışma durumunda güncellenecek sütunlardır.
10. Global Scope ve Yerel Scope'lar
Tekrar eden filtreleri model seviyesinde tanımlayın:
// Model içinde
protected static function booted(): void
{
static::addGlobalScope('published', function ($builder) {
$builder->where('published', true);
});
}
Global scope'lar her sorguya eklenir. Bu davranışı withoutGlobalScope('published') ile kaldırabilirsiniz. Global scope'ları gizli filtre olarak kullanmak, ileride beklenmedik sonuçlar doğurabilir; belgelendirin.
11. exists ve doesntExist
Bir kaydın varlığını kontrol ederken count() veya get() kullanmayın:
// Kötü
if (Post::where('slug', $slug)->count() > 0) { /* ... */ }
// İyi
if (Post::where('slug', $slug)->exists()) { /* ... */ }
exists veritabanına LIMIT 1 gönderir ve tüm satırları çekmez.
12. İlişkilerde touch ve Ters İlişki
belongsTo ilişkilerinde inverse tanımlamak, eager loading'in doğru çalışmasını sağlar:
class Comment extends Model
{
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
class Post extends Model
{
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
Bir yorum eklendiğinde üst kaydın updated_at değerini güncellemek için $touches kullanın:
class Comment extends Model
{
protected $touches = ['post'];
}
Bu, önbellek invalidation senaryolarında işe yarar; ancak her yorum eklemede ek bir UPDATE sorgusu üretir.
13. Önbellekleme
Sık okunan, seyrek değişen veriler için sorgu sonuçlarını önbelleğe alın:
$posts = Cache::remember('published_posts', 3600, function () {
return Post::with('user')->where('published', true)->get();
});
Güvenlik ve doğruluk notları:
- Önbellek anahtarlarını kullanıcı girdisiyle doğrudan oluşturmayın; anahtar çakışması ve yetkisiz veri sızıntısına yol açabilir. Kullanıcıya özel verilerde anahtara kullanıcı kimliğini dahil edin.
- Model değiştiğinde önbelleği geçersiz kılmak için
Model::saved/deletedolaylarını veya observer'ları kullanın.
14. selectRaw ve Dikkat Edilmesi Gerekenler
Hesaplanmış sütunlar için selectRaw kullanılabilir:
Post::selectRaw('user_id, COUNT(*) as post_count')
->groupBy('user_id')
->get();
Güvenlik uyarısı: selectRaw, whereRaw, orderByRaw gibi metotlara kullanıcı girdisini asla doğrudan geçirmeyin. Bağlama (binding) kullanın:
Post::whereRaw('LOWER(title) = ?', [strtolower($title)])->get();
Bağlama olmadan kullanılan ham SQL, SQL enjeksiyonuna açıktır.
15. exists Optimizasyonu ve whereIn Büyüklüğü
whereIn ile binlerce kimlik geçirmek sorguyu yavaşlatır ve bazı veritabanlarında parametre sınırına takılır. Bu durumlarda geçici tablo, join veya whereIn yerine alt sorgu tercih edilebilir:
Post::whereIn('user_id', function ($query) {
$query->select('id')->from('users')->where('active', true);
})->get();
16. Yaygın Hatalar Özeti
- Döngü içinde ilişki erişimi (N+1).
withkullanırken ilişki sütunlarında birincil/yabancı anahtarı atlamak.get()ile tüm tabloyu belleğe almak.- Toplu
insert/updatesonrası model olaylarının çalıştığını varsaymak. whereRawiçine kullanıcı girdisini bağlamasız gömmek.- Global scope'ları gizli iş kuralı olarak kullanıp belgelendirmemek.
- Önbellek anahtarlarına kullanıcıya özel veriyi dahil etmemek.
17. Optimizasyon Akışı
- Yavaş sorguları
DB::listenveya Debugbar ile tespit edin. - N+1 var mı kontrol edin;
with,withCount,withWhereHasile giderin. - Yalnızca gerekli sütunları seçin.
EXPLAINile indeks kullanımını doğrulayın; eksik indeksleri ekleyin.- Büyük veri kümelerinde
chunkById,cursorveyalazykullanın. - Toplu işlemlerde
insert,update,upserttercih edin; olay ve timestamp davranışını göz önünde bulundurun. - Sık okunan veriler için önbellekleme ekleyin ve invalidation stratejisini belirleyin.
- Değişikliği ölçün; iyileştirme yoksa geri alın.
Kapanış
Eloquent optimizasyonu, sihirli bir ayar değil, ölçüm ve doğrulama döngüsüdür. Buradaki tekniklerin çoğu tek başına büyük kazanç sağlamaz; ancak doğru indeksler ve ölçülmüş sorgularla birleştiğinde uygulamanın ölçeklenebilirliğini belirgin biçimde etkiler. Her değişikliği kendi veri hacminizde test edin; bu rehberdeki hiçbir öneri, sizin şemanız ve erişim kalıplarınız üzerinde ölçüm yapılmadan kesin bir kazanç olarak kabul edilmemelidir.