Pagination'da OFFSET Tuzağı
20. sayfa, 20 × LIMIT kadar iş

page ve pageSize client'ın bildiği dil. OFFSET (page-1)*pageSize da SQL'in bildiği kısa yol. İlk sayfada index + LIMIT 20. Yirminci sayfada planner yine 400 satırı okuyup 380'ini atar. Atmak ucuz değil; o satırlar index'ten (veya heap'ten) geçmek zorunda.
Neden ilk sayfada görünmez
Ürün listesi çoğu oturumda page=1. p95 iyi. SEO botu ve 'daha fazla' zinciri page=12'ye inince aynı endpoint 15 ms'den 180 ms'ye çıkar. Ortalama yalan söyler, derin sayfa gerçek yükü taşır.
-- pahalı: her sayfa baştan sayar
SELECT id, title, published_at
FROM posts
WHERE status = 'published'
ORDER BY published_at DESC, id DESC
OFFSET 380 LIMIT 20;
-- keyset: bir önceki sayfanın son satırı
SELECT id, title, published_at
FROM posts
WHERE status = 'published'
AND (published_at, id) < ($1, $2)
ORDER BY published_at DESC, id DESC
LIMIT 20;
(published_at, id) karşılaştırması bileşik index ile Index Scan + Limit olur. OFFSET yok. Sayfa atlamak ('direkt 20') keyset ile doğal değil; cursor ister. Bunu gizleyip hâlâ ?page=20 sunmak, sunucuda OFFSET'e çevirmek demek. O zaman geçmemişim.
API sözleşmesi değişir
Client artık nextCursor taşır. Toplam sayfa sayısı keyset'ten ucuza gelmez; COUNT(*) ayrı ve pahalı. '1200 yazı / 20 = 60 sayfa' göstergesini istiyorsan onu ayrı cache'lersin veya bırakırsın. Sözleşmeyi baştan söylemezsem frontend page state'ini silmek zorunda kalır.
public sealed record PostCursor(DateTimeOffset PublishedAt, Guid Id);
app.MapGet("/posts", async (DateTimeOffset? afterAt, Guid? afterId, AppDb db, CancellationToken ct) =>
{
var query = db.Posts.AsNoTracking().Where(p => p.Status == "published");
if (afterAt is { } at && afterId is { } id)
query = query.Where(p => p.PublishedAt < at
|| (p.PublishedAt == at && p.Id.CompareTo(id) < 0));
var rows = await query
.OrderByDescending(p => p.PublishedAt).ThenByDescending(p => p.Id)
.Take(21)
.Select(p => new { p.Id, p.Title, p.PublishedAt })
.ToListAsync(ct);
var page = rows.Take(20).ToList();
var next = rows.Count > 20 ? new PostCursor(page[^1].PublishedAt, page[^1].Id) : null;
return Results.Ok(new { items = page, next });
});
Silinen satır
OFFSET, sen sayfalarken silinen satır yüzünden kaydırır: 7. sayfada aynı yazıyı iki kez görürsün. Keyset, silinen cursor'ın arkasından devam eder; tekrar etmez, atlar. Bu çoğu listede daha dürüst. 'Sayfa numarası' istiyorsan tutarlılık zaten yoktu.
Feed, admin tablo, sonsuz scroll: keyset. Raporun '3. sayfası' ve sabit satır numarası: OFFSET kalabilir, ama 10 binden sonra değil. page=1'in ucuzluğu, page=20'nin faturasını ödemez.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap