Laravel Veritabanı Sorgularını Nasıl Optimize Ederiz ?
KAYNAK: KARABAY A, 2021 . Laravel Veritabanı Sorgularını Nasıl Optimize Ederiz ?,
https://www.karabayyazilim.com/public/blog/laravel-veritabani-sorgularini-nasil-optimize-ederiz-2021-03-21-185044
(Erişim Tarihi : 21 Mart 2021).
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.
1. Seçenek: Chunk
// 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.
2. Seçenek : Cursor
// 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.
3. Seçenek: chunkById
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
}
});
Chunk
ve chunkById
Arasındaki büyük fark id
alana 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 chunk
ve chunkById
aş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 id
bir 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 posts
Veritabanında sorgu yürütür- Aldığı
Post
her satır için yeni bir model nesnesi oluşturur (Sorgu oluşturucu için bir PHP standart nesnesi oluşturur) Post
Modellerle 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, Post
her satır için model oluşturma ve bu nesneler için bir koleksiyon oluşturma ek yüküne sahiptir . Post
Modele 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 posts
Veritabanı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 collection
nesneye 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, authors
ilişkiyi yükleme konusunda istekli olsak da, hala daha fazla sorgulama yapıyor. Çünkü team
iliş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 posts
ve authors
. Yazılar tablosunda, author_id
yazarlar 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 posts
, private_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_posts
yalnı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ı posts
tablodan, diğeri kayıtları private_posts
tablodan almak için .
private_posts
Tablodaki 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, simplePaginate
toplam satır sayısını saymaz ve sorgu paginate
yaklaşı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, paginate
yerine 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, date
işlev değerlendirilene kadar nerede koşulu uygulanmadığı için tam bir tablo taramasıyla sonuçlanacaktır .
date
Aş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ı
database
ziyaret 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.
Comments
Post a Comment