Laravel Veritabanı Sorgularını Nasıl Optimize Ederiz ?

Uygulamanız yavaş çalışıyorsa veya çok sayıda veritabanı sorgusu yapıyorsa, uygulama yükleme sürenizi iyileştirmek için aşağıdaki performans optimizasyonu ipuçlarını izleyin.

1. Büyük veri kümelerini alma

Bu ipucu esas olarak, büyük veri kümeleriyle uğraşırken uygulamanızın bellek kullanımını iyileştirmeye odaklanır.

Uygulamanızın büyük bir veri kümesini işlemesi gerekiyorsa, hepsini bir kerede almak yerine, sonuçların bir alt kümesini alabilir ve bunları gruplar halinde işleyebilirsiniz.

Bir tablodan büyük bir sonuç kümesini almak için posts, genellikle aşağıdakileri yaparız.

$posts = Post::all(); 
$posts = DB::table('posts')->get();
foreach ($posts as $post) {
// Process posts
}

Yukarıdaki örnekler, post tablosundaki tüm kayıtları alacak ve işleyecektir. Ya bu tabloda 1 milyon satır varsa? Kolayca ram’iniz tükenir.

Büyük veri kümeleriyle uğraşırken sorunları önlemek için, sonuçların bir alt kümesini alabilir ve aşağıdaki gibi işleyebilirsiniz.

// when using eloquent
$posts = Post::chunk(100, function($posts){
foreach ($posts as $post){
// Process posts
}
});
// when using query builder
$posts = DB::table('posts')->chunk(100, function ($posts){
foreach ($posts as $post){
// Process posts
}
});

Yukarıdaki örnek, post tablosundan 100 kayıt alır, bunları işler, başka 100 kayıt alır ve bunları işler. Bu döngü, tüm kayıtlar işlenene kadar devam edecektir.

Bu yaklaşım, daha fazla veritabanı sorgusu yapacak, ancak belleği oldukça verimli hale getirecektir. Genellikle büyük veri kümelerinin işlenmesi arka planda yapılacaktır. Bu nedenle, büyük veri kümelerini işlerken belleğin tükenmesini önlemek için arka planda çalışırken daha fazla sorgu yapmakta sorun yoktur.

// when using eloquent
foreach (Post::cursor() as $post){
// Process a single post
}
// when using query builder
foreach (DB::table('posts')->cursor() as $post){
// Process a single post
}

Yukarıdaki örnek, tek bir veritabanı sorgusu yapacak, tablodan tüm kayıtları alacak ve anlamlı modelleri tek tek işleyecektir. Bu yaklaşım, tüm gönderileri almak için yalnızca bir veritabanı sorgusu yapacaktır. Ancak bellek kullanımını optimize etmek için php generators kullanır .

bunu ne zaman kullanabilirsin

Bu, uygulama düzeyinde bellek kullanımını büyük ölçüde optimize etse de, bir tablodaki tüm girişleri aldığımız için veritabanı örneğindeki bellek kullanımı yine de daha yüksek olacaktır.

Cursor kullanmak daha iyidir uygulamanızı çalıştıran web uygulamanızın belleği daha azsa ve veritabanı örneğinde daha fazla bellek varsa bunu kullanabilirsiniz. Ancak, veritabanı örneğinizde yeterli bellek yoksa, chunk kullanmak daha iyidir.

Not: Bu özellik yalnızca laravel 8 ve üzeri sürümlerde mevcuttur.

// when using eloquent
$posts = Post::chunkById(100, function($posts){
foreach ($posts as $post){
// Process posts
}
});
// when using query builder
$posts = DB::table('posts')->chunkById(100, function ($posts){
foreach ($posts as $post){
// Process posts
}
});

Chunkve chunkById Arasındaki büyük fark idalana dayalı olarak veritabanı sonuçlarını alır . Bu kimlik alanı genellikle bir tamsayı alanıdır ve çoğu durumda otomatik olarak artan bir alan olacaktır.

Sorgular tarafından yapılan chunkve chunkByIdaşağıdaki gibidir.

Chunk

select * from posts offset 0 limit 100

select * from posts offset 101 limit 100

chunkById

select * from posts order by id asc limit 100

select * from posts where id > 100 order by id asc limit 100

Genellikle limit ve offset daha yavaştır ve bunları kullanmaktan kaçınmaya çalışın. ChunkById bir tamsayı alanı olan id alanını kullandığından ve sorgu kullandığından where clause, sorgu çok daha hızlı olacaktır.

chunkById’yi ne zaman kullanabilirsiniz? — Uygulamanız laravel 8 veya üzeri çalışıyorsa — Veritabanı tablonuzda idbir tamsayı alanı olan bir sütun varsa kullanabilirsiniz .

2. Yalnızca ihtiyacınız olan sütunları seçin

Genellikle bir veritabanı tablosundan sonuçları almak için aşağıdakileri yapardık.

$posts = Post::find(1); 
$posts = DB::table('posts')->where('id','=',1)->first();

Yukarıdaki kod aşağıdaki gibi bir sorgu ile sonuçlanacaktır

select * from posts where id = 1 limit 1

Gördüğünüz gibi, sorgu bir select *. Bu, veritabanı tablosundan tüm sütunları aldığı anlamına gelir. Tablodaki tüm sütunlara gerçekten ihtiyacımız varsa bu sorun değil.

Ama bunun yerine, yalnızca belirli sütunlara ihtiyacımız varsa (id, title), aşağıdaki gibi yalnızca bu sütunları alabiliriz.

$posts = Post::select(['id','title'])->find(1); $posts = DB::table('posts')->where('id','=',1)->select(['id','title'])->first(); 

Yukarıdaki kod aşağıdaki gibi bir sorgu ile sonuçlanacaktır

select id,title from posts where id = 1 limit 1

3. Veritabanından tam olarak bir veya iki sütuna ihtiyaç duyduğunuzda pluck kullanın

Bu ipucu, sonuçlar veritabanından alındıktan sonra harcanan zamana daha fazla odaklanmaktadır. Bu, gerçek sorgu süresini etkilemez.

$posts = Post::select(['title','slug'])->get(); $posts = DB::table('posts')->select(['title','slug'])->get(); 

Yukarıdaki kod çalıştırıldığında, arka planda aşağıdakileri yapar.

  • select title, slug from postsVeritabanında sorgu yürütür
  • Aldığı Posther satır için yeni bir model nesnesi oluşturur (Sorgu oluşturucu için bir PHP standart nesnesi oluşturur)
  • PostModellerle yeni bir koleksiyon oluşturur
  • Koleksiyonu döndürür

Şimdi, sonuçlara yazdıralım.

foreach ($posts as $post){
$post->title;
$post->slug;
}

Yukarıdaki yaklaşım, Posther satır için model oluşturma ve bu nesneler için bir koleksiyon oluşturma ek yüküne sahiptir . PostModele gerçekten ihtiyacınız varsa bu en iyisi olacaktır . Ancak ihtiyacınız olan tek şey bu iki değerse, aşağıdakileri yapabilirsiniz.

$posts = Post::pluck('title', 'slug');$posts = DB::table('posts')->pluck('title','slug'); 

Yukarıdaki kod çalıştırıldığında, arka planda aşağıdakileri yapar.

  • select title, slug from postsVeritabanında sorgusunu çalıştırır.
  • Bir dizi oluşturur array key.

Şimdi, sonuçlara erişmek için yapardık

foreach ($posts as $slug => $title){
$title
$slug
}

Yalnızca bir sütunu almak istiyorsanız, şunları yapabilirsiniz:

$posts = Post::pluck('title'); $posts = DB::table('posts')->pluck('title'); foreach ($posts as $title){
$title
}

Yukarıdaki yaklaşım Post, her satır için nesnelerin oluşturulmasını ortadan kaldırır . Böylece bellek kullanımını ve sorgu sonuçlarını işlemek için harcanan zamanı azaltır.

4. Koleksiyon yerine sorgu kullanarak satırları sayın

Bir tablodaki toplam satır sayısını saymak için normalde

$posts = Post::all()->count(); $posts = DB::table('posts')->get()->count(); 

Bu, aşağıdaki sorguyu oluşturacaktır

select * from posts

Yukarıdaki yaklaşım tablodan tüm satırları alacak, bunları bir collectionnesneye yükleyecek ve sonuçları sayacaktır. Bu, veritabanı tablosunda daha az satır olduğunda iyi çalışır. Ancak tablo büyüdükçe hafızamız hızla tükenecek.

Yukarıdaki yaklaşım yerine, veritabanının kendisindeki toplam satır sayısını doğrudan sayabiliriz.

$posts = Post::count(); $posts = DB::table('posts')->count(); 

Bu, aşağıdaki sorguyu oluşturacaktır

select count(*) from posts

SQL’de satırları saymak yavaş bir işlemdir ve veritabanı tablosunda çok fazla satır olduğunda çok kötü performans gösterir. Mümkün olduğunca satır saymaktan kaçınmak daha iyidir.

5. İstekli yükleme ilişkileri ile N + 1 sorgularından kaçının

Bu ipucunu milyonlarca kez duymuş olabilirsiniz. Bu yüzden olabildiğince kısa ve basit tutacağım. Aşağıdaki senaryoya sahip olduğunuzu varsayalım

class PostController extends Controller
{
public function index()
{
$posts = Post::all();
return view('posts.index', ['posts' => $posts ]);
}
}
// posts/index.blade.php file@foreach($posts as $post)
<li>
<h3>{{ $post->title }}</h3>
<p>Author: {{ $post->author->name }}</p>
</li>
@endforeach

Yukarıdaki kod, tüm gönderileri almak ve yazının başlığını ve yazarını web sayfasında görüntülemektir. Yukarıdaki kod author, post modelinizle bir ilişkiniz olduğunu varsayar .

Yukarıdaki kodun yürütülmesi, aşağıdaki sorguların çalıştırılmasına neden olacaktır.

select * from posts // Assume this query returned 5 posts
select * from authors where id = { post1.author_id }
select * from authors where id = { post2.author_id }
select * from authors where id = { post3.author_id }
select * from authors where id = { post4.author_id }
select * from authors where id = { post5.author_id }

Gördüğünüz gibi, gönderileri almak için bir sorgumuz ve gönderilerin yazarlarını almak için 5 sorgumuz var (5 gönderimiz olduğunu varsaydığımıza göre) Dolayısıyla, aldığı her gönderi için, yazarını almak için ayrı bir sorgu yapıyor.

Dolayısıyla, N sayıda gönderi varsa, N + 1 sorgu yapar (gönderileri almak için 1 sorgu ve her gönderi için yazarı almak için N sorgu). Bu genellikle N + 1 sorgu problemi olarak bilinir.

Bundan kaçınmak için, yazarın ilişkisini aşağıdaki gibi yayınlara istekli olarak yükleyin.

$posts = Post::with(['author'])->get(); 

Yukarıdaki kodun yürütülmesi, aşağıdaki sorguların çalıştırılmasına neden olacaktır.

select * from posts // Assume this query returned 5 posts
select * from authors where id in( { post1.author_id }, { post2.author_id }, { post3.author_id }, { post4.author_id }, { post5.author_id } )

6. İstekli yük iç içe geçmiş ilişki

Yukarıdaki örnekten yazarın bir takıma ait olduğunu ve takım adını da göstermek istediğinizi düşünün. Yani blade dosyasında aşağıdaki gibi yaparsınız.

@foreach($posts as $post)
<li>
<h3>{{ $post->title }}</h3>
<p>Author: {{ $post->author->name }}</p>
<p>Author's Team: {{ $post->author->team->name }}</p>
</li>
@endforeach

Şimdi controllerda aşağıdaki gibi yapıyoruz.

$posts = Post::with(['author'])->get();

Aşağıdaki sorgularla sonuçlanacak.

select * from posts // Assume this query returned 5 posts
select * from authors where id in( { post1.author_id }, { post2.author_id }, { post3.author_id }, { post4.author_id }, { post5.author_id } )
select * from teams where id = { author1.team_id }
select * from teams where id = { author2.team_id }
select * from teams where id = { author3.team_id }
select * from teams where id = { author4.team_id }
select * from teams where id = { author5.team_id }

Gördüğünüz gibi, authorsilişkiyi yükleme konusunda istekli olsak da, hala daha fazla sorgulama yapıyor. Çünkü teamilişkiyi yüklemek için istekli yüklemiyoruz.

Aşağıdaki gibi yaparak bunu düzeltebiliriz.

$posts = Post::with(['author.team'])->get();

Yukarıdaki kodun yürütülmesi, aşağıdaki sorguların çalıştırılmasına neden olacaktır.

select * from posts // Assume this query returned 5 posts
select * from authors where id in( { post1.author_id }, { post2.author_id }, { post3.author_id }, { post4.author_id }, { post5.author_id } )
select * from teams where id in( { author1.team_id }, { author2.team_id }, { author3.team_id }, { author4.team_id }, { author5.team_id } )

Dolayısıyla, iç içe geçmiş ilişkiyi yükleyerek, toplam sorgu sayısını 11'den 3'e düşürdük.

7. OwnTo ilişkisini yüklemeyin, sadece id’ye ihtiyacınız varsa yükleyin

İki tablonuz olduğunu varsayalım postsve authors. Yazılar tablosunda, author_idyazarlar tablosunda bir OwnTo ilişkisini temsil eden bir sütun bulunur.

Bir gönderinin yazar id’sini almak için normalde aşağıdaki gibi yapardık

$post = Post::findOrFail(<post id>);$post->author->id;

Bu, iki sorgunun yürütülmesine neden olur.

select * from posts where id = <post id> limit 1
select * from authors where id = <post author id> limit 1

Bunun yerine, aşağıdakileri yaparak doğrudan yazar id’sini alabilirsiniz.

$post = Post::findOrFail(<post id>);$post->author_id; 

Yukarıdaki yaklaşımı ne zaman kullanabilirim?

Yazarlar tablosunda bir satırın her zaman varolduğundan emin olduğunuzda yukarıdaki yaklaşımı kullanabilirsiniz.

8. Gereksiz sorgulardan kaçının

Çoğu zaman, gerekli olmayan veritabanı sorguları yaparız. Aşağıdaki örnekteki gibi.

<?phpclass PostController extends Controller
{
public function index()
{
$posts = Post::all();
$private_posts = PrivatePost::all();
return view('posts.index', ['posts' => $posts, 'private_posts' => $private_posts ]);
}
}

Yukarıdaki kod iki farklı tablolardan satır alınıyor olduğu postsprivate_posts) ve bunları görüntülemek için geçen. View dosyası aşağıdaki gibi görünür.

// posts/index.blade.php@if( request()->user()->isAdmin() )
<h2>Private Posts</h2>
<ul>
@foreach($private_posts as $post)
<li>
<h3>{{ $post->title }}</h3>
<p>Published At: {{ $post->published_at }}</p>
</li>
@endforeach
</ul>
@endif
<h2>Posts</h2>
<ul>
@foreach($posts as $post)
<li>
<h3>{{ $post->title }}</h3>
<p>Published At: {{ $post->published_at }}</p>
</li>
@endforeach
</ul>

Yukarıda görebileceğiniz gibi, $private_postsyalnızca admin görebiliyor tüm kullanıcılar bu gönderileri göremez.

$posts = Post::all();
$private_posts = PrivatePost::all();

İki sorgu yapıyoruz. Biri kayıtları poststablodan, diğeri kayıtları private_poststablodan almak için .

private_postsTablodaki kayıtlar yalnızca admin user. görür ancak biz bu kayıtları tüm kullanıcılar için almak için sorgulama yapıyoruz.

Bu fazladan sorguyu önlemek için aşağıdaki gibi yapabilirsiniz.

$posts = Post::all();$private_posts = collect();if( request()->user()->isAdmin() ){
$private_posts = PrivatePost::all();
}

Mantığımızı yukarıdaki gibi değiştirerek, yönetici kullanıcı için iki sorgu ve diğer tüm kullanıcılar için bir sorgu yapıyoruz.

9. Benzer sorguları bir araya getirin

Bazen aynı tablodan farklı türdeki satırları almak için sorgular yapmamız gerekir.

$published_posts = Post::where('status','=','published')->get();
$featured_posts = Post::where('status','=','featured')->get();
$scheduled_posts = Post::where('status','=','scheduled')->get();

Yukarıdaki kod, aynı tablodan farklı durumdaki satırları alıyor. Kod, aşağıdaki sorguların yapılmasına neden olacaktır.

select * from posts where status = 'published'
select * from posts where status = 'featured'
select * from posts where status = 'scheduled'

Gördüğünüz gibi kayıtları geri almak için aynı tabloya 3 farklı sorgu yapıyor. Yalnızca bir veritabanı sorgusu yapmak için bu kodu yeniden düzenleyebiliriz.

$posts = Post::whereIn('status',['published', 'featured', 'scheduled'])->get();$published_posts = $posts->where('status','=','published');
$featured_posts = $posts->where('status','=','featured');
$scheduled_posts = $posts->where('status','=','scheduled');
select * from posts where status in ( 'published', 'featured', 'scheduled' )

Yukarıdaki kod, belirtilen durumlardan herhangi birine sahip tüm gönderileri almak için tek bir sorgu yapmak ve döndürülen gönderileri durumlarına göre filtreleyerek her durum için ayrı koleksiyonlar oluşturmaktır. Dolayısıyla, durumlarıyla birlikte hala üç farklı değişkenimiz olacak ve yalnızca bir sorgu yapmış oluyoruz.

10. Sayfalandırmak yerine simplePaginate kullanın

Sonuçları sayfalandırırken genellikle aşağıdaki yöntemi kullanırız.

$posts = Post::paginate(20);

Bu 2 sorgu yapacak. 1 sayfalandırılmış sonuçları almak için ve diğeri tablodaki toplam satır sayısını saymak için. Bir tablodaki satırları saymak yavaş bir işlemdir ve sorgu performansını olumsuz yönde etkiler.

Öyleyse neden laravel toplam satır sayısını sayar?

Sayfalandırma bağlantıları oluşturmak için Laravel toplam satır sayısını sayar. Dolayısıyla, sayfalandırma bağlantıları oluşturulduğunda, orada kaç sayfanın olacağını ve geçmiş sayfa numarasının ne olduğunu önceden bilirsiniz. Böylece istediğiniz sayfaya kolayca gidebilirsiniz.

Öte yandan, simplePaginatetoplam satır sayısını saymaz ve sorgu paginateyaklaşımdan çok daha hızlı olacaktır . Ancak son sayfa numarasını bilme ve farklı sayfalara geçme yeteneğinizi kaybedeceksiniz.

Veritabanı tablonuzda çok sayıda satır varsa, paginateyerine simplePaginate yapmak daha iyidir.

$posts = Post::paginate(20); $posts = Post::simplePaginate(20); 

11. Baştaki joker karakterler kullanmaktan kaçının (LIKE anahtar kelime)

Belirli bir modelle eşleşen sonuçları sorgulamaya çalışırken, genellikle

select * from table_name where column like %keyword%

Yukarıdaki sorgu, tam bir tablo taramasıyla sonuçlanacaktır. Anahtar kelimenin sütun değerinin başında geçtiğini bilirsek, sonuçları aşağıdaki gibi sorgulayabiliriz.

select * from table_name where column like keyword%

12. Where cümlesinde SQL işlevlerini kullanmaktan kaçının

Her zaman tam tablo taramasıyla sonuçlandığından, where cümlesindeki SQL işlevlerinden kaçınmak her zaman daha iyidir. Aşağıdaki örneğe bakalım. Sonuçları belirli bir tarihe göre sorgulamak için genellikle

$posts = POST::whereDate('created_at', '>=', now() )->get();

Bu, aşağıdakine benzer bir sorgu ile sonuçlanacaktır

select * from posts where date(created_at) >= 'timestamp-here'

Yukarıdaki sorgu, dateişlev değerlendirilene kadar nerede koşulu uygulanmadığı için tam bir tablo taramasıyla sonuçlanacaktır .

dateAşağıdaki gibi sql işlevinden kaçınmak için bunu yeniden düzenleyebiliriz

$posts = Post::where('created_at', '>=', now() )->get();select * from posts where created_at >= 'timestamp-here'

13. Bir tabloya çok fazla sütun eklemekten kaçının

Bir tablodaki toplam sütun sayısını sınırlamak daha iyidir. Mysql gibi ilişkisel veritabanları, çok sütunlu tabloları birden çok tabloya bölmek için kullanılabilir. Birincil ve yabancı anahtarlarını kullanarak bir araya getirilebilirler.

Bir tabloya çok fazla sütun eklemek, bireysel kayıt uzunluğunu artıracak ve tablo taramasını yavaşlatacaktır. Bir select *sorgu yaptığınızda , gerçekten ihtiyacınız olmayan bir grup sütunu alırsınız.

14. Bir tablodan en son satırları almanın daha iyi yolu

Bir tablodan en son satırları almak istediğimizde, genellikle

$posts = Post::latest()->get();
// or $posts = Post::orderBy('created_at', 'desc')->get();

Yukarıdaki yaklaşım aşağıdaki sql sorgusunu üretecektir.

select * from posts order by created_at desc

Sorgu temelde satırları created_at sütununa göre azalan sırada sıralar. created_at sütunu dizge tabanlı bir sütun olduğundan, sonuçları bu şekilde sıralamak genellikle daha yavaştır.

Veritabanı tablonuzda otomatik olarak artan bir birincil anahtar kimliği varsa, çoğu durumda en son satır her zaman en yüksek kimliğe sahip olacaktır. İd alanı bir tamsayı alanı ve aynı zamanda bir birincil anahtar olduğundan, sonuçları bu anahtara göre sıralamak çok daha hızlıdır. Dolayısıyla, en son satırları almanın daha iyi yolu aşağıdaki gibidir.

$posts = Post::latest('id')->get();
// or $posts = Post::orderBy('id', 'desc')->get();
select * from posts order by id desc

15. Sorguları inceleyin ve optimize edin

Laravel’de sorguları optimize ederken tek bir evrensel çözüm yoktur. Uygulamanızın ne yaptığını, kaç sorgu yaptığını, kaçının gerçekten kullanımda olduğunu yalnızca siz bilirsiniz. Bu nedenle, uygulamanız tarafından yapılan sorguları incelemek, yapılan toplam sorgu sayısını belirlemenize ve azaltmanıza yardımcı olacaktır.

Her sayfada yapılan sorguları incelemenize yardımcı olacak aşağıdaki aracı kullanabilirsiniz.

  • Laravel Debugbar — Laravel debugbar, bir sayfayıdatabaseziyaret ettiğinizde yürütülen tüm sorguları görüntüleyen bir sekmeye sahiptir. Uygulamanızdaki tüm sayfaları ziyaret edin ve her sayfada yürütülen sorgulara bakın.

  Alıntı Yap

KARABAY A, 2021 . Laravel Veritabanı Sorgularını Nasıl Optimize Ederiz ?,

https://www.karabayyazilim.com/blog/php/framework/laravel/laravel-veritabani-sorgularini-nasil-optimize-ederiz-2021-03-21-185044

(Erişim Tarihi : 21 Mart 2021).


  Bu yazıyı paylaş

Yorumlar (1)

  • berat Yanıtla

    hocam iyi dökütrmüşsünüz. Devamını bekleriz. :)

    8 ay önce

    Yorum Yap

    Abone Ol
    Blog yazılarımdan ilk siz haberdar olmak için email bültenine kaydolun