MySQL ve MariaDB’de Sayfalama için LIMIT OFFSET Kullanımı

Veritabanı yönetiminin en sık karşılaşılan pratik problemlerinden biri, büyük veri setlerini kullanıcıya sayfa sayfa sunmaktır. Bir e-ticaret sitesinde binlerce ürünü tek sorguda çekmek hem sunucuyu hem de ağı gereksiz yere yorar. İşte bu noktada LIMIT ve OFFSET devreye girer. MariaDB ve MySQL’de sayfalama mekanizmasının nasıl çalıştığını, tuzaklarını ve gerçek dünya senaryolarındaki kullanımını bu yazıda ele alacağız.

LIMIT ve OFFSET Nedir?

LIMIT, bir SQL sorgusunun kaç satır döndüreceğini belirler. OFFSET ise kaç satırın atlanacağını söyler. İkisi birlikte kullanıldığında klasik sayfalama mantığını oluşturur.

Temel sözdizimi şu şekildedir:

SELECT kolon1, kolon2
FROM tablo_adi
ORDER BY id
LIMIT satir_sayisi OFFSET atlanan_satir;

Alternatif olarak MariaDB ve MySQL’de kısa yazım da kullanılabilir:

SELECT kolon1, kolon2
FROM tablo_adi
ORDER BY id
LIMIT atlanan_satir, satir_sayisi;

Dikkat edin, kısa yazımda sıralama tersine döner. Önce offset, sonra limit gelir. Bu ufak fark, yeni başlayanların sıkça düştüğü bir tuzaktır.

Sayfalama Mantığı Nasıl Çalışır?

Diyelim ki bir blog platformu yönetiyorsunuz ve makaleler tablonuzda 10.000 kayıt var. Kullanıcıya sayfa başına 20 makale göstermek istiyorsunuz.

  • 1. sayfa: OFFSET 0, LIMIT 20
  • 2. sayfa: OFFSET 20, LIMIT 20
  • 3. sayfa: OFFSET 40, LIMIT 20
  • N. sayfa: OFFSET (N-1) * 20, LIMIT 20

Bu formülü uygulayan temel bir örnek:

-- 1. sayfa
SELECT id, baslik, yazar, yayın_tarihi
FROM makaleler
ORDER BY yayın_tarihi DESC
LIMIT 20 OFFSET 0;

-- 3. sayfa
SELECT id, baslik, yazar, yayın_tarihi
FROM makaleler
ORDER BY yayın_tarihi DESC
LIMIT 20 OFFSET 40;

-- 7. sayfa
SELECT id, baslik, yazar, yayın_tarihi
FROM makaleler
ORDER BY yayın_tarihi DESC
LIMIT 20 OFFSET 120;

Burada ORDER BY kullanmak zorunludur. Sırası belirsiz bir veri setinde sayfalama yapmak tutarsız sonuçlar doğurur. Aynı satırlar farklı sayfalarda tekrar edebilir veya bazı satırlar hiç görünmeyebilir.

Gerçek Dünya Senaryosu 1: E-Ticaret Ürün Listesi

Bir müşteri için yönettiğiniz e-ticaret platformunda ürünler tablosunda 50.000’den fazla kayıt var. Ürün listeleme sayfası her yüklenişte tüm ürünleri çekmeye çalışıyordu ve sayfa 8-10 saniyede açılıyordu. Sayfalama eklendikten sonra bu süre 200 ms’nin altına indi.

-- Kategori bazlı ürün listesi, sayfa 1 (sayfa başına 24 ürün)
SELECT
    u.id,
    u.urun_adi,
    u.fiyat,
    u.stok_adedi,
    k.kategori_adi
FROM urunler u
INNER JOIN kategoriler k ON u.kategori_id = k.id
WHERE u.aktif = 1
  AND u.kategori_id = 5
ORDER BY u.fiyat ASC
LIMIT 24 OFFSET 0;

-- Toplam sayfa sayısını hesaplamak için
SELECT COUNT(*) AS toplam_kayit
FROM urunler
WHERE aktif = 1
  AND kategori_id = 5;

Toplam kayıt sayısını bilerek toplam sayfa sayısını şu şekilde hesaplarsınız:

Toplam sayfa = CEIL(toplam_kayit / sayfa_boyutu)

Bu sorguyu uygulama katmanında PHP, Python veya Node.js’te ayrı olarak çalıştırıp sonucu cache’lemek, her sayfalama isteğinde COUNT sorgusu atmaktan çok daha verimlidir.

Gerçek Dünya Senaryosu 2: Log Tablosu Sorguları

Sistem loglarını veritabanında tutan bir yapıda çalışıyorsanız, log tablosu hızla milyonlarca satıra ulaşır. Bir yönetim panelinde log görüntüleme sayfası şöyle kurgulanabilir:

-- Hata loglarını filtreli sayfalama ile getir
SELECT
    log_id,
    log_seviyesi,
    mesaj,
    kaynak_ip,
    olusturma_tarihi
FROM sistem_loglari
WHERE log_seviyesi IN ('ERROR', 'CRITICAL')
  AND olusturma_tarihi >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY olusturma_tarihi DESC
LIMIT 50 OFFSET 150;

Bu sorguda dikkat edilmesi gereken şey, WHERE koşulunun sayfalama ile birleşmesidir. Filtreleme yapıldığında toplam kayıt sayısı değişir, dolayısıyla sayfa hesaplamalarını her zaman filtrelenmiş COUNT üzerinden yapmanız gerekir.

LIMIT OFFSET’in Performans Tuzakları

Burası yazının en kritik bölümü. Pek çok geliştirici ve sysadmin, LIMIT OFFSET’in her durumda verimli çalıştığını zanneder. Oysa büyük OFFSET değerlerinde ciddi performans sorunları ortaya çıkar.

Neden yavaşlar? MySQL ve MariaDB, OFFSET kullanıldığında önce atlanacak tüm satırları okur, sonra onları atar. Yani OFFSET 50000 LIMIT 20 dediğinizde veritabanı motoru 50.020 satırı tarar ama size sadece 20 tanesini verir. Bu, indeks kullanılsa bile ciddi bir yük oluşturur.

Bunu test etmek için EXPLAIN kullanabilirsiniz:

-- Küçük offset - hızlı
EXPLAIN SELECT id, baslik FROM makaleler
ORDER BY id DESC
LIMIT 20 OFFSET 10;

-- Büyük offset - yavaş
EXPLAIN SELECT id, baslik FROM makaleler
ORDER BY id DESC
LIMIT 20 OFFSET 49980;

EXPLAIN çıktısında “rows” kolonuna bakın. Büyük OFFSET’te taranacak satır sayısı çok daha yüksek çıkacaktır.

Performansı Artırmak İçin İndeksleme

Sayfalama sorgularında ORDER BY kolonunun indeksli olması kritiktir. Aşağıdaki örneği inceleyelim:

-- Önce mevcut indeksleri kontrol edin
SHOW INDEX FROM makaleler;

-- Yoksa ORDER BY kolonuna indeks ekleyin
ALTER TABLE makaleler ADD INDEX idx_yayin_tarihi (yayın_tarihi);

-- Bileşik indeks genellikle daha verimlidir
ALTER TABLE makaleler ADD INDEX idx_aktif_tarih (aktif, yayın_tarihi);

-- Sorgunun indeksi kullanıp kullanmadığını doğrulayın
EXPLAIN SELECT id, baslik, yayın_tarihi
FROM makaleler
WHERE aktif = 1
ORDER BY yayın_tarihi DESC
LIMIT 20 OFFSET 0;

İyi bir indeks yapısı ile küçük OFFSET değerlerinde neredeyse anlık sonuç alırsınız.

Keyset Pagination: Büyük Veri Setleri İçin Gerçek Çözüm

Büyük OFFSET değerlerinin yarattığı performans sorununu çözmek için Keyset Pagination (Cursor-based Pagination) yöntemi kullanılır. Bu yöntemde OFFSET yerine son görüntülenen kaydın anahtarı kullanılır.

-- Klasik LIMIT OFFSET yöntemi (yavaş, büyük offset'lerde)
SELECT id, baslik, yayın_tarihi
FROM makaleler
ORDER BY id DESC
LIMIT 20 OFFSET 50000;

-- Keyset Pagination yöntemi (hızlı, her zaman)
-- Önceki sayfanın son kaydının id'si 49500 olsun
SELECT id, baslik, yayın_tarihi
FROM makaleler
WHERE id < 49500
ORDER BY id DESC
LIMIT 20;

Keyset yöntemi neredeyse her zaman indeksi tam olarak kullanır ve OFFSET yönteminin yaşadığı tarama sorununu tamamen ortadan kaldırır. Ancak bu yöntemin de sınırlamaları vardır:

  • Rastgele bir sayfaya doğrudan atlanamaz (örneğin “57. sayfaya git” özelliği)
  • Sıralama kriterleri birden fazlaysa ve değerler tekrar ediyorsa uygulama karmaşıklaşır
  • Kullanıcı “ileri/geri” yerine “daha fazla yükle” modeline ihtiyaç duyar

Bu yüzden hangi yöntemi seçeceğiniz, uygulamanızın gereksinimine bağlıdır. API’ler ve mobil uygulamalar için keyset tercih edilirken, geleneksel sayfa numaralı web arayüzleri için LIMIT OFFSET daha pratiktir.

Gerçek Dünya Senaryosu 3: Raporlama Sistemi

Büyük bir şirketin muhasebe sistemini yönetiyorsunuz. Yüz binlerce fatura kaydı var ve finans ekibi bu faturaları filtreleyip sayfa sayfa incelemek istiyor.

-- Ödenmemiş faturaları tutara göre sıralı, sayfalı getir
SELECT
    f.fatura_no,
    f.musteri_adi,
    f.toplam_tutar,
    f.vade_tarihi,
    f.durum,
    DATEDIFF(NOW(), f.vade_tarihi) AS gecikme_gunu
FROM faturalar f
WHERE f.durum = 'ODENMEDI'
  AND f.vade_tarihi < NOW()
ORDER BY f.vade_tarihi ASC, f.toplam_tutar DESC
LIMIT 30 OFFSET 60;

-- Bu sorgu için bileşik indeks önerilir
-- CREATE INDEX idx_durum_vade ON faturalar (durum, vade_tarihi);

Burada ORDER BY iki kolonla yapılmaktadır. Bu durumlarda her iki kolonun da indeks içinde yer alması önemlidir.

Stored Procedure ile Sayfalama

Sıkça kullanılan sayfalama sorgularını stored procedure içine almak, hem tekrar kullanılabilirliği artırır hem de uygulama katmanındaki SQL enjeksiyon riskini azaltır.

DELIMITER //

CREATE PROCEDURE sayfalı_urun_listesi(
    IN p_kategori_id INT,
    IN p_sayfa INT,
    IN p_sayfa_boyutu INT
)
BEGIN
    DECLARE v_offset INT;
    SET v_offset = (p_sayfa - 1) * p_sayfa_boyutu;

    -- Ana veri seti
    SELECT
        u.id,
        u.urun_adi,
        u.fiyat,
        u.stok_adedi
    FROM urunler u
    WHERE u.aktif = 1
      AND (p_kategori_id = 0 OR u.kategori_id = p_kategori_id)
    ORDER BY u.urun_adi ASC
    LIMIT p_sayfa_boyutu OFFSET v_offset;

    -- Toplam kayıt sayısı
    SELECT COUNT(*) AS toplam
    FROM urunler
    WHERE aktif = 1
      AND (p_kategori_id = 0 OR kategori_id = p_kategori_id);
END //

DELIMITER ;

-- Kullanımı
CALL sayfalı_urun_listesi(5, 3, 24);
-- Kategori 5, 3. sayfa, sayfa başına 24 kayıt

Bu stored procedure iki sonuç seti döndürür: biri sayfa verisi, diğeri toplam kayıt sayısı. Uygulama tarafında her ikisini de tek çağrıda alırsınız.

SQL_CALC_FOUND_ROWS Kullanımı ve Uyarısı

Eski MySQL/MariaDB versiyonlarında SQL_CALC_FOUND_ROWS ile hem veri hem de toplam sayıyı tek sorguda elde etmeye çalışmak popülerdi:

-- SQL_CALC_FOUND_ROWS kullanımı (artık önerilmiyor)
SELECT SQL_CALC_FOUND_ROWS id, baslik, yayın_tarihi
FROM makaleler
WHERE aktif = 1
ORDER BY yayın_tarihi DESC
LIMIT 20 OFFSET 0;

-- Hemen ardından
SELECT FOUND_ROWS();

Uyarı: MariaDB ve MySQL 8.0+ sürümlerinde SQL_CALC_FOUND_ROWS deprecated (kullanımdan kaldırılmakta) olarak işaretlenmiştir. Performans açısından da ayrı bir COUNT sorgusu atmaktan genellikle daha yavaş çalışır. Bu yüzden iki ayrı sorgu kullanmak veya store procedure yaklaşımını tercih etmek daha sağlıklıdır.

Dinamik Sayfalama ile WHERE Koşulları

Gerçek uygulamalarda kullanıcı filtreleme yapar ve bu filtreler dinamik olarak değişir. Aşağıda iş hayatından bir senaryo:

-- Çalışan arama: isim, departman ve maaş aralığı filtreli sayfalama
SELECT
    c.id,
    c.ad_soyad,
    c.email,
    c.maas,
    d.departman_adi,
    c.ise_giris_tarihi
FROM calisanlar c
INNER JOIN departmanlar d ON c.departman_id = d.id
WHERE c.aktif = 1
  AND (c.ad_soyad LIKE '%ahmet%' OR c.email LIKE '%ahmet%')
  AND c.maas BETWEEN 15000 AND 50000
  AND c.departman_id IN (2, 4, 7)
ORDER BY c.maas DESC
LIMIT 25 OFFSET 25;

Bu tür dinamik sorgularda dikkat edilmesi gereken nokta şudur: LIKE '%ahmet% gibi baştaki yüzde işaretli arama ifadeleri indeksi kullanamaz. Bu durumda Full-Text Search veya Elasticsearch gibi bir çözüme yönelmek daha uygun olabilir.

MariaDB’ye Özgü ROWS EXAMINED İpucu

MariaDB’de büyük tablolarda sayfalama sorgularını optimize ederken ANALYZE ve EXPLAIN EXTENDED oldukça yardımcıdır:

-- MariaDB'de sorgu analizini detaylı yapmak için
EXPLAIN EXTENDED
SELECT id, baslik, yayın_tarihi
FROM makaleler
WHERE aktif = 1
ORDER BY yayın_tarihi DESC
LIMIT 20 OFFSET 100;

-- Uyarıları görmek için
SHOW WARNINGS;

-- MariaDB 10.1+ için ANALYZE kullanımı
ANALYZE SELECT id, baslik
FROM makaleler
ORDER BY id DESC
LIMIT 20 OFFSET 500;

ANALYZE komutu sorguyu gerçekten çalıştırır ve gerçek satır sayılarını gösterir. Tahmin ile gerçek arasındaki fark büyükse istatistikleri güncellemek gerekebilir:

-- Tablo istatistiklerini güncelle
ANALYZE TABLE makaleler;

Sık Yapılan Hatalar

ORDER BY olmadan sayfalama yapmak tutarsız sonuçlar verir. Veritabanı motoru sıralamayı garanti etmez.

Büyük OFFSET değerlerini gözardı etmek performans sorunlarına yol açar. Eğer uygulamanızda kullanıcılar 500. sayfaya kadar geçiyorsa, keyset pagination’a geçmeyi ciddi olarak düşünmelisiniz.

Her sayfa isteğinde COUNT sorgusu atmak gereksiz yük oluşturur. Toplam sayıyı cache’lemek (Redis veya Memcached ile birkaç dakikalığına) genellikle yeterlidir.

LIMIT değerini kullanıcıya bırakmak tehlikelidir. Bir kullanıcı LIMIT 1000000 gönderirse sisteminiz çökebilir. Maksimum sayfa boyutu her zaman uygulama katmanında sınırlandırılmalıdır:

-- Uygulama katmanında güvenli LIMIT kontrolü (örnek mantık)
-- Kullanıcıdan gelen: sayfa_boyutu = 1000 (kötü niyetli veya hatalı)
-- Güvenli maksimum: 100

SET @maks_limit = 100;
SET @istenen_limit = 1000;
SET @gercek_limit = LEAST(@istenen_limit, @maks_limit);

SELECT id, baslik
FROM makaleler
ORDER BY id DESC
LIMIT @gercek_limit OFFSET 0;

Sonuç

LIMIT OFFSET sayfalama, MariaDB ve MySQL’de en temel ve en sık kullanılan tekniklerden biridir. Küçük ve orta ölçekli veri setlerinde son derece iyi çalışır, uygulaması basittir ve uygulama geliştiricilerin hemen anlayacağı sezgisel bir yapıya sahiptir.

Ancak veri setiniz büyüdükçe ve özellikle offset değerleri yükseldikçe performans sorunlarıyla karşılaşırsınız. Bu noktada dikkat etmeniz gereken birkaç kritik kural vardır:

  • Her zaman ORDER BY kullanın
  • Sıralama ve filtreleme kolonlarını indeksleyin
  • Toplam sayfa sayısını her sorguda hesaplamak yerine cache’leyin
  • Büyük veri setleri veya yüksek offset değerleri için keyset pagination’ı değerlendirin
  • Maksimum LIMIT değerini her zaman uygulama katmanında kontrol altında tutun
  • SQL_CALC_FOUND_ROWS yerine ayrı COUNT sorgusu kullanın

Bir sysadmin olarak veritabanı sorgularını sadece “çalışıyor mu” diye değil, “bu sorgu 100 bin kayıtta da bu hızda çalışır mı?” diye de sorgulamanız gerekir. EXPLAIN komutu sizin en yakın dostunuzdur. Sorgu planını okumayı alışkanlık haline getirdiğinizde, sayfalama kaynaklı performans sorunlarını üretim ortamına taşımadan yakalayabilirsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir