Bir listeleme sorgusu iki saniyede dönüyorsa, ilk tepki çoğu zaman sunucuyu büyütmek olur. Oysa sorunun kaynağı çoğunlukla başka bir yerdedir: veritabanı, sorguyu çalıştırmak için tabloyu baştan sona okumak zorunda kalmıştır. Bu, pahalı bir donanım yükseltmesiyle değil, doğru bir indeksle çözülebilecek bir sorundur.

Bu metin, yavaş bir yazı listesi sorgusunun nasıl elli milisaniyeye indiğini anlatıyor. Süreç sandığınızdan daha sıkıcıdır: ölç, planı oku, indeksi düzelt. Ama her adımı atlamadan yaparsanız, sonuç hem öngörülebilir hem de tekrarlanabilir olur.

Ölçmeden başlamayın

Yavaş bir sorgunun ne kadar yavaş olduğunu bilmeden onu düzeltmeye çalışmak, karanlıkta bir şey aramaya benzer. Önce sorguyu tekrar tekrar çalıştırıp ortalama ve en kötü süreleri kaydedin. Aynı zamanda sorgunun hangi parametrelerle çağrıldığını da not edin: bir kategori için mi, bir yazar için mi, sayfa numarasıyla mı?

Bu ölçüm, bir iyileşmenin gerçek olup olmadığını da gösterir. Yalnızca bir kez hızlı çalıştığında "düzeldi" demek yanıltıcıdır; birkaç yüz tekrarda ortalamanın ve dağılımın değişip değişmediğine bakmak gerekir.

ÖlçümNe anlatır
Ortalama süreTipik kullanıcı deneyimi
95. yüzdelikŞikâyet eden kullanıcıların deneyimi
Taranan satır sayısıSorgunun ne kadar iş yaptığı
SıklıkSaniyede kaç kez çalıştığı

Plan ne söylüyor?

Sorgu planı, veritabanının sorguyu nasıl çalıştırmayı tasarladığını gösterir. Başlangıç için iki soruya cevap aramak yeterlidir: hangi indeks kullanılıyor ve kaç satır okunuyor? Plan ayrıca sıralama için geçici bir alan açılıp açılmadığını da söyler.

EXPLAIN SELECT id, title, published_at
FROM posts
WHERE status = 'published' AND category_id = 2
ORDER BY published_at DESC, id DESC
LIMIT 8;

Plan çıktısında bir tam tablo taramasının göründüğü, satır sayısının yüz binlerle ifade edildiği ve ayrıca bir sıralama adımının bulunduğu bir sorgu, tipik bir indeks eksikliğidir. Bu üçlü bir alarm gibi okunmalıdır.

Bileşik indeksin sütun sırası

Bir sorgu birden fazla kolonu süzüp sıraladığında, tek tek kolonlara indeks eklemek yeterli değildir. Bileşik indeksin sütun sırası, veritabanının satırları ne kadar verimli bulacağını belirler. Bu sıra için pratik bir kural vardır: önce eşitlik filtreleri, sonra aralık filtreleri, en sonda sıralama kolonu gelir.

CREATE INDEX posts_status_category_published_idx
    ON posts (status, category_id, published_at, id);

Bu sırayla veritabanı önce durum ve kategoriyle doğru satır bloğuna gider. Bu blok içindeki published_at ve id değerleri indeksin kendi sırasında zaten tutulduğu için sıralama için ek bir iş gerekmez. Sıra bozulduğunda indeks kullanılsa bile sıralama için tüm satırlar yeniden düzenlenir.

Bir indeks bir hız değil, bir takas kararıdır: okuma kazanır, yazma öder.
  • Eşitlik filtresi olan kolonlar indeksin başına gelir.
  • Aralık filtresi (>, <, BETWEEN) bir kolon sonra gelir; ondan sonraki kolonlar bu aralık içinde sıralı değildir.
  • Sıralama kolonu, yukarıdaki iki kuralı bozmadan eklenir.

Kapsayan indeks ve tabloya dönüş

Bir indeks, sorguya gereken bütün kolonları içeriyorsa, veritabanı asıl tabloya hiç dönmek zorunda kalmaz. Buna kapsayan indeks denir. Liste sayfasında yalnızca birkaç kolon okunduğunda bu yöntem büyük fark yaratır.

Ama her kolonu indekse koymak akıllıca değildir. Kapsayan indeksin boyutu büyür, yazma işlemleri pahalılaşır ve bellekte daha az satır tutulur. Dengeyi bulmak için yalnızca gerçekten listelenen kolonları eklemek yeterlidir.

DurumÖnerilen yaklaşım
Liste yalnızca başlık ve tarih gösteriyorBu kolonları da içeren kapsayan indeks
Liste tam kayıt gerektiriyorKlasik indeks, tabloya dönüş kabul edilebilir
Yazma yoğun bir tabloİndeks sayısını sınırlı tutun

Kolonun üzerine fonksiyon koymak

En sık yapılan indeks hatalarından biri, kolonu bir fonksiyonun içine koymaktır. WHERE DATE(published_at) = '2026-10-01' gibi bir koşul, published_at üzerindeki indeksi kullanılamaz hâle getirir. Veritabanı her satır için fonksiyonu hesaplamak zorunda kalır ve indeks bu hesabın içine giremez.

Çözüm çoğu zaman koşulu yeniden yazmaktır. Yukarıdaki örnek, aralık koşuluyla yazıldığında indeks yeniden devreye girer:

WHERE published_at >= '2026-10-01 00:00:00'
  AND published_at < '2026-10-02 00:00:00'

Benzer bir tuzak, başında joker karakter olan LIKE sorgularıdır. LIKE '%kelime' bir indeksin sıralamasından faydalanamaz. Metin araması için tam metin indeksi ya da uygun bir arama altyapısı gerekir.

Sayfalama: büyük ofsetlerden kaçınmak

Sayfalamada OFFSET kullanıldığında, veritabanı her sayfa için önceki tüm satırları atlamak zorundadır. Sayfa numarası büyüdükçe sorgu da yavaşlar. Yüzüncü sayfada on binlerce satır okunup atılır.

Bunun yerine, son görülen kaydın değerine göre ilerleyen bir sayfalama yöntemi kullanılabilir. Bu yöntemde sorgu, önceki sayfanın son kaydından sonrasını doğrudan ister ve indeks bu noktadan itibaren okur.

SELECT id, title, published_at
FROM posts
WHERE status = 'published' AND category_id = 2
  AND (published_at, id) < ('2026-09-14 09:30:00', 412)
ORDER BY published_at DESC, id DESC
LIMIT 8;

Bu yöntemin bir bedeli var: kullanıcı doğrudan 47. sayfaya atlayamaz. Ama bir yazı listesinde çoğu kişi zaten sonraki sayfaya ya da "daha fazla yükle" düğmesine basar. Bu kullanım biçimine uygun bir yöntem, performansı belirgin biçimde iyileştirir.

İndeksin maliyeti

Her indeks, veritabanına bir yük getirir. Bir satır eklendiğinde ya da güncellendiğinde, her ilgili indeksin de güncellenmesi gerekir. Bu yüzden indeks eklemek kadar, gereksiz olanları kaldırmak da bir sorumluluktur.

Kullanılmayan indeksleri bulmak için veritabanının kullanım istatistiklerine bakmak yeterlidir. Bir indeks aylarca hiç okunmadıysa, hem yazma maliyetini hem de bellek payını boşa harcıyor demektir.

  • Yeni indeks eklemeden önce, mevcut indekslerin bu sorguya uyup uymadığını kontrol edin.
  • Benzer iki indeksten biri, diğerinin başlangıç kolonlarını içeriyorsa gereksiz olabilir.
  • Üç ayda bir kullanım istatistiklerini gözden geçirin ve kullanılmayanları kaldırın.

Seçicilik: kolon gerçekten ayırt ediyor mu?

Bir indeksin işe yaraması, kolonun satırları ne kadar iyi ayırt ettiğine bağlıdır. Buna seçicilik denir. Yalnızca iki değer alan bir durum kolonu, tek başına bakıldığında tabloyu yarıya indirir. Milyonlarca farklı değer alan bir kimlik kolonu ise tek bir satırı bulmak için mükemmeldir.

Seçiciliği ölçmek için kolondaki farklı değerlerin sayısına bakmak yeterlidir. Oran sıfıra yakınsa, kolon tek başına indeks için zayıf bir adaydır; ama bileşik indeksin ikinci ya da üçüncü kolonu olarak anlamlı olabilir.

SELECT COUNT(DISTINCT status) / COUNT(*) AS durum_secicilik,
       COUNT(DISTINCT category_id) / COUNT(*) AS kategori_secicilik
FROM posts;

Bu ölçüm, bir kolonun neden tek başına indekslenmemesi gerektiğini açıklar. Ama bileşik indekste bir kolonun konumu ile seçiciliği birlikte düşünülmelidir. Eşitlik filtresi olan düşük seçicilikli bir kolon, ondan sonra gelen yüksek seçicilikli bir kolonla birlikte iyi sonuç verebilir.

Birleştirmelerde indeks: ilişkiler de sorgudur

İki tabloyu birleştiren bir sorgu, her iki tarafta da indeksin varlığından yararlanabilir. En sık ihmal edilen alan, yabancı anahtar kolonlarıdır. Bir yazının kategorisine ya da etiketine göre filtreleme yapılıyorsa, ilişki tablosundaki kolonların indekslenmiş olması gerekir.

Laravel'de bir yazı ile etiketler arasındaki ilişki, post_tag adlı bir ara tabloda tutulur. Bu tablo için iki yönlü bir bakış gerekir: etiketten yazıya gidilen sorgular ile yazıdan etikete gidilen sorgular farklı indeksler ister.

-- Yazının etiketlerini bulmak
CREATE INDEX post_tag_post_tag_idx ON post_tag (post_id, tag_id);

-- Etiketin yazılarını bulmak
CREATE INDEX post_tag_tag_post_idx ON post_tag (tag_id, post_id);

Bu iki indeks, ara tablonun her iki yönde de hızlı okunmasını sağlar. Birincil anahtarın ilk kolonu zaten bir yönü karşılıyorsa, ikinci indeks gereksiz olabilir. Plan çıktısı bunu doğrulamanın en güvenilir yoludur.

Yazma yoğun tablolarda toplu işlemler

Her yeni satır, tablodaki her indeksi de günceller. Bir yazının yorumları, görüntülemeleri ya da bildirimleri sık sık yazılıyorsa, indeks sayısı doğrudan yazma hızını etkiler. Bu tür tablolarda indeks sayısını sınırlı tutmak ve toplu yazmalara geçmek, hem veritabanına hem de uygulamaya fayda sağlar.

// Tek tek yazmak yerine parça parça toplu ekleme
collect($ziyaretler)
    ->chunk(500)
    ->each(fn ($parca) => PostView::query()->insert($parca->all()));

Toplu ekleme, her satır için ayrı bir işlem yerine tek bir işlemde birçok satır yazar. Bu, yazma maliyetini düşürür; ama parça boyutunu çok büyük tutmak, bellek ve kilit süresi açısından başka bir sorun yaratabilir. Pratikte birkaç yüz satırlık parçalar çoğu zaman dengeyi bulur.

Sorguyu koddan çıkarmak

Yavaş bir sorgu fark ettiğinizde, onu üreten kod yolunu bulmak gerekir. Laravel'de sorgu oluşturucu, toSql() ile SQL metnini ve getBindings() ile parametrelerini verir. Bu iki çıktıyı bir araya getirmek, günlükteki sorguyu koddaki satıra bağlamanın en hızlı yoludur.

$sorgu = Post::query()
    ->published()
    ->inCategory('gezi')
    ->latestFirst();

$sorgu->toSql();      // SQL metni
$sorgu->getBindings(); // parametreler

Bu yaklaşım, sorgu kapsamlarının neyi ürettiğini de gösterir. Bir kapsam yanlış bir koşul ekliyorsa ya da gereksiz bir birleştirme yapıyorsa, SQL çıktısı bunu açıkça ortaya koyar.

İndeks kararını belgelemek

Bir indeks eklendiğinde, neden eklendiği kaydedilmelidir. Bir migration dosyasına kısa bir yorum yazmak, altı ay sonra kimsenin "bu neden burada?" diye sormasını önler. Yorum, hangi sorgunun bu indeksten yararlandığını ve hangi ölçümle doğrulandığını söylemelidir.

  • İndeks adlarını tablo, kolonlar ve amaçla birlikte standart bir biçimde verin.
  • Bir indeks kaldırıldığında da kaldırma nedenini aynı yerde belirtin.
  • Ölçüm sonuçlarını kısa bir not olarak ekleyin: önce ve sonra süre.

Sonuç: bir sorgunun hikâyesi

Bu süreçte değişen şey yalnızca bir indeksti. Uygulama kodu ve sonuçlar aynı kaldı; yalnızca veritabanının satırları nasıl bulacağı netleşti. İki saniyelik bir liste elli milisaniyede çalışmaya başladı. Ölçüm öncesi ve sonrası aynı veriyle alındığında bu iyileşme tartışmaya yer bırakmadı.

Yavaş sorgularla başa çıkmak için kısa bir liste:

  1. Sorguyu, parametreleriyle birlikte, tekrar tekrar ölçün.
  2. Planı okuyun: tam tarama, geçici alan ve yüksek satır sayısına bakın.
  3. Bileşik indeksi eşitlik, aralık ve sıralama sırasıyla kurun.
  4. Kolonun üzerine fonksiyon koymayın; koşulu aralıkla yazın.
  5. Büyük sayfa numaralarında ofset yerine son kayda göre ilerleyin.
  6. Kullanılmayan indeksleri düzenli olarak temizleyin.