Bir liste ekranındaki binlerce kaydı tek sayfada göstermek hem tarayıcıyı hem veritabanını boğar. İşte burada Laravel pagination devreye girer: sorgu sonuçlarını sayfalara bölüp her istekte yalnızca ihtiyaç duyulan parçayı çeker. Laravel bu işi üç farklı metotla yapar ve hangisini seçtiğin, uygulamanın hem performansını hem de kullanıcı deneyimini doğrudan etkiler. Bu yazıda offset tabanlı ve cursor tabanlı sayfalama arasındaki farkı, ne zaman hangisini kullanman gerektiğini ve gerçek kod örnekleriyle nasıl uygulayacağını anlatıyorum.
Üç pagination metodu: paginate, simplePaginate, cursorPaginate
Eloquent ve query builder üç hazır metot sunar. Aralarındaki fark, kullanıcıya kaç bilgi gösterdikleri ve veritabanına nasıl yük bindirdikleridir:
paginate()— Klasik offset sayfalama. Toplam kayıt sayısını da hesaplar, böylece "1 2 3 … 50" gibi numaralı sayfa bağlantıları ve toplam sayfa adedini gösterebilirsin.simplePaginate()— Yine offset tabanlı, ama toplam sayıyı saymaz. Sadece "Önceki / Sonraki" düğmeleri üretir; sayım sorgusu olmadığı için daha hızlıdır.cursorPaginate()— Cursor (imleç) tabanlı sayfalama. Offset yerine son satırın sütun değerlerini referans alır; çok büyük tablolarda en hızlı ve en tutarlı yöntemdir.
Offset sayfalama nasıl çalışır?
Offset yaklaşımı SQL'de LIMIT ve OFFSET kullanır. 20'şerlik sayfalarda 5. sayfayı istediğinde Laravel LIMIT 20 OFFSET 80 üretir. En yaygın ve en kolay yöntemdir:
// Controller
public function index()
{
$posts = \App\Models\Post::query()
->where('published', true)
->orderByDesc('created_at')
->paginate(20);
return view('posts.index', compact('posts'));
}
Blade tarafında bağlantıları tek satırda basarsın. Tailwind veya Bootstrap görünümünü php artisan vendor:publish --tag=laravel-pagination ile özelleştirebilirsin:
<div class="posts">
@foreach ($posts as $post)
<article>{{ $post->title }}</article>
@endforeach
</div>
{{ $posts->links() }}
Avantajı: kullanıcı doğrudan herhangi bir sayfaya atlayabilir ve toplam kaç sayfa olduğunu görür. Dezavantajı ise ölçeklenince ortaya çıkar.
Offset'in iki büyük sorunu
Veritabanı büyüdükçe offset iki belaya yol açar:
- Yavaşlayan sorgular:
OFFSET 100000dediğinde veritabanı atlanacak 100.000 satırı yine de taramak zorundadır. Derin sayfalar gözle görülür şekilde yavaşlar. - Kayan kayıtlar: Sen 2. sayfadayken yeni bir kayıt eklenirse, tüm satırlar bir aşağı kayar. Sonraki sayfada daha önce gördüğün bir kaydı tekrar görür veya bir kaydı tamamen atlarsın.
Listeler genelde "yeniden eskiye" sıralandığı ve sürekli yeni içerik eklendiği için bu kayma sorunu sandığından daha sık yaşanır.
Cursor sayfalama: büyük tablolar için doğru seçim
Cursor pagination, offset yerine bir referans noktası kullanır. "Şu kayıttan sonrakileri ver" der ve WHERE koşuluyla devam eder; bu yüzden atlanan satır taraması olmaz. Laravel cursor değerini şifreleyip URL'ye koyar:
$posts = \App\Models\Post::query()
->where('published', true)
->orderByDesc('id')
->cursorPaginate(20);
Arka planda üretilen sorgu kabaca şöyledir; offset yoktur, sadece indeksli bir karşılaştırma vardır:
select * from posts
where published = 1 and id < 480
order by id desc
limit 21;
Önemli kurallar:
- Sıralama şarttır ve kararlı olmalı:
orderByalanı benzersiz olmalı (örneğinid). Tekrarlı değerlere göre sıralarsan, eşitlik kırıcı ikinci bir sütun ekle:orderBy('created_at')->orderBy('id'). - Rastgele sayfaya atlanamaz: Cursor yalnızca ileri/geri gider; "8. sayfa" bağlantısı veremezsin. Sonsuz kaydırma (infinite scroll) ve "Daha fazla yükle" düğmeleri için idealdir.
- Toplam sayı yoktur: Tıpkı
simplePaginategibi sayım yapmaz, bu yüzden hızlıdır.
Hangisini ne zaman kullanmalı?
Karar aslında basit; iki soruyla netleşir: kullanıcının numaralı sayfalara atlamasına gerek var mı ve tablo ne kadar büyük?
- Yönetim panelleri, arama sonuçları: Sayfa numaraları ve toplam adet beklenir →
paginate(). - Mobil akışlar, sosyal feed, "Daha fazla yükle": İleri doğru sonsuz kaydırma →
cursorPaginate(). - Basit listeler, çok büyük tablo ama numara gerekmiyor:
simplePaginate()ya da cursor.
Genel kural: yüz binlerce satıra giden tablolarda mümkünse cursor'a geç; ama numaralı navigasyon ürün için gerçekten gerekliyse offset'te kal ve sorgularını indekslerle destekle.
API ve performans ipuçları
JSON API yazıyorsan paginator'ı doğrudan döndürmen yeterli; Laravel data, links ve meta alanlarını otomatik üretir. API Resource ile birlikte de çalışır:
return \App\Http\Resources\PostResource::collection(
Post::where('published', true)->cursorPaginate(20)
);
Birkaç pratik öneri:
- Sıralama ve filtre yaptığın sütunlara veritabanı indeksi ekle; cursor'ın hızı buna bağlıdır.
- Sadece gereken kolonları seç:
select('id', 'title', 'created_at'). - İlişkili veriyi
with()ile eager-load et ki sayfa başına N+1 sorgu patlamasın. - Sayfa boyutunu kullanıcı girdisinden alıyorsan üst sınır koy (örn. en fazla 100).
Sık Sorulan Sorular
Cursor pagination ile toplam sayfa sayısını gösterebilir miyim?
Hayır. Cursor (ve simplePaginate) sayım sorgusu çalıştırmadığı için toplam kayıt veya sayfa adedini bilmez. Toplam sayıya mutlaka ihtiyacın varsa offset tabanlı paginate() kullan ya da sayımı ayrı, cache'lenmiş bir sorguyla yap.
cursorPaginate çıktısını numaralı sayfa bağlantılarına çevirebilir miyim?
Hayır. Cursor doğası gereği yalnızca komşu sayfalara (önceki/sonraki) gidebilir; rastgele sayfaya atlama yapamaz. Numaralı navigasyon şartsa offset sayfalamada kalman gerekir.
Çok büyük tablolarda offset neden yavaşlar?
OFFSET N komutu veritabanına ilk N satırı bulup atmasını söyler. N büyüdükçe atlanacak satır sayısı da büyür, dolayısıyla iş yükü artar. Cursor ise indeksli bir WHERE koşuluyla doğrudan doğru noktadan başladığı için sabit hızda kalır.
Sayfalama mimarini doğru kurmak, ölçeklenen bir uygulamanın temelidir. Projende offset'ten cursor'a geçişi ya da yavaş liste sorgularının optimizasyonunu konuşmak istersen benimle iletişime geç.