Tüm yazılar

#veritabanı#postgresql#sql

Sorgunuz Neden 8 Saniye Sürüyor? Veritabanı İndeksi ile 8 Milisaniyeye İnmenin Rehberi

Uygulamanız ilk günlerde hızlıydı, veri büyüdükçe yavaşladı mı? Çoğu zaman suçlu eksik bir indekstir. Veritabanı indeksinin nasıl çalıştığını, EXPLAIN ile nasıl teşhis koyulacağını ve sık yapılan hataları PostgreSQL örnekleriyle anlatıyoruz.

Sorgunuz Neden 8 Saniye Sürüyor? Veritabanı İndeksi ile 8 Milisaniyeye İnmenin Rehberi
İçindekiler 10

Uygulamanızı yayına aldığınız ilk ay her şey şimşek gibiydi. Sipariş listesi anında açılıyor, arama anında sonuç veriyordu. Bir yıl sonra aynı sayfa 8 saniyede açılıyor, müşteriler şikâyet ediyor, sunucunun işlemcisi sürekli %100'de. Kod değişmedi. Değişen tek şey veri: 5 bin sipariş, 5 milyon oldu.

Bu hikâyenin sonu çoğu zaman şaşırtıcı derecede basit bir satırdır: CREATE INDEX. Bu yazıda indekslerin nasıl çalıştığını, doğru indeksi nasıl seçeceğinizi ve bir sorgunun neden yavaş olduğunu nasıl teşhis edeceğinizi PostgreSQL örnekleriyle anlatıyoruz. Anlattıklarımızın büyük kısmı MySQL, SQL Server ve diğer ilişkisel veritabanları için de geçerli.

Kısaca:

  • İndeks olmadan veritabanı, aradığını bulmak için tablodaki her satırı okur (sequential scan). 5 milyon satırda bu saniyeler sürer.
  • İndeks, bir kitabın arkasındaki dizin gibi çalışır: veritabanı doğrudan ilgili satırlara atlar.
  • EXPLAIN ANALYZE, bir sorgunun nasıl çalıştığını ve zamanın nereye gittiğini gösterir. Tahmin etmeyin, ölçün.
  • İndeksler bedava değil: her indeks yazma işlemlerini yavaşlatır ve disk kaplar. Her sütuna indeks eklemek çözüm değil.

İndeks nedir? Telefon rehberi benzetmesi

Kalın bir telefon rehberinde "Yılmaz, Ahmet"i arıyorsunuz. Rehber soyadına göre alfabetik sıralı olduğu için doğrudan "Y" bölümüne gidersiniz; birkaç saniyede bulursunuz.

Şimdi aynı rehberde telefon numarası 0532 123 45 67 olan kişiyi arayın. Rehber numaraya göre sıralı değil. Tek yolunuz ilk sayfadan başlayıp her satırı tek tek kontrol etmek.

Veritabanı tablosu, sıralanmamış bu ikinci durum gibidir. Bir sütuna indeks eklediğinizde, veritabanı o sütunun değerlerini sıralı bir yapıda ayrıca tutar ve her değerin tablodaki yerini işaretler. Arama yapıldığında önce bu sıralı yapıya bakar, sonra doğrudan ilgili satırlara gider.

PostgreSQL'de varsayılan indeks türü B-tree'dir. B-tree, dengeli bir ağaç yapısıdır: milyonlarca satırlık bir tabloda bile aranan değere birkaç adımda ulaşılır. Tablo 10 kat büyüdüğünde arama süresi 10 kat değil, çok az artar.

Teşhis: EXPLAIN ANALYZE

Yavaş bir sorguyu hızlandırmanın ilk adımı, neden yavaş olduğunu görmektir. PostgreSQL'de sorgunun başına EXPLAIN ANALYZE eklediğinizde veritabanı sorguyu çalıştırır ve izlediği yolu raporlar.

Örnek: 5 milyon satırlık bir siparisler tablosunda bir müşterinin siparişlerini arıyoruz.

EXPLAIN ANALYZE
SELECT * FROM siparisler WHERE musteri_id = 48213;

İndeks yokken çıktı şuna benzer (süreler örnektir, donanıma göre değişir):

Seq Scan on siparisler  (cost=0.00..104186.00 rows=52 width=64)
                        (actual time=12.4..812.6 rows=47 loops=1)
  Filter: (musteri_id = 48213)
  Rows Removed by Filter: 4999953
Execution Time: 812.9 ms

Burada iki kritik ipucu var: Seq Scan (tüm tablo baştan sona okundu) ve Rows Removed by Filter: 4999953 (47 satır bulmak için yaklaşık 5 milyon satır okunup atıldı). Bu tablo, eksik indeksin imzasıdır.

İndeksi ekleyelim:

CREATE INDEX idx_siparisler_musteri_id ON siparisler (musteri_id);

Aynı sorgunun yeni planı:

Index Scan using idx_siparisler_musteri_id on siparisler
                        (actual time=0.03..0.21 rows=47 loops=1)
  Index Cond: (musteri_id = 48213)
Execution Time: 0.25 ms

812 milisaniyeden 0,25 milisaniyeye. Kodda tek satır değişmeden, yaklaşık 3.000 kat hızlanma.

Hangi sütunlara indeks eklenmeli?

İyi adaylar:

  • WHERE koşullarında sık kullanılan sütunlar (musteri_id, durum, email),
  • JOIN işlemlerinde kullanılan yabancı anahtarlar. PostgreSQL birincil anahtara otomatik indeks oluşturur, ama yabancı anahtar sütunlarına oluşturmaz. Bu, en sık gözden kaçan eksiktir.
  • ORDER BY ile sıralanan sütunlar, özellikle LIMIT ile birlikte ("son 20 sipariş").

Zayıf adaylar:

  • Çok az farklı değeri olan sütunlar tek başına (ör. yalnızca "aktif/pasif" değeri alan bir sütun). Satırların yarısını döndüren bir indeks, tabloyu baştan okumaktan pek hızlı değildir.
  • Çok küçük tablolar. Birkaç yüz satırlık bir tabloda sıralı okuma zaten hızlıdır.

Bileşik indeks: Sıra her şeydir

Sorgunuz birden fazla sütuna göre filtreliyorsa, bileşik indeks (birden çok sütunlu) kullanabilirsiniz:

CREATE INDEX idx_siparisler_musteri_tarih
    ON siparisler (musteri_id, olusturma_tarihi);

Bu indeks, telefon rehberinin "önce soyada, sonra ada göre" sıralanması gibidir. Dolayısıyla:

Sorgu İndeks kullanılır mı?
WHERE musteri_id = 5 Evet
WHERE musteri_id = 5 AND olusturma_tarihi > '2026-01-01' Evet, en verimli hâli
WHERE musteri_id = 5 ORDER BY olusturma_tarihi DESC LIMIT 20 Evet, sıralama da indeksten gelir
WHERE olusturma_tarihi > '2026-01-01' Genellikle verimli kullanılmaz

Son satırın nedeni basit: rehberde yalnızca ada göre arama yaparsanız soyadı sıralaması işinize yaramaz. Kural şu: eşitlikle filtrelenen sütun başa, aralıkla filtrelenen ya da sıralanan sütun sona.

İndeksi boşa çıkaran sık hatalar

İndeks var ama kullanılmıyor mu? En yaygın nedenler:

1. Sütunu fonksiyonla sarmak. email sütununda indeks olsa bile şu sorgu onu kullanamaz:

SELECT * FROM kullanicilar WHERE lower(email) = 'ayse@ornek.com';

İndeks email değerlerine göre sıralı, lower(email) değerlerine göre değil. Çözüm, ifade indeksi:

CREATE INDEX idx_kullanicilar_email_lower ON kullanicilar (lower(email));

Aynı sorun tarih sütunlarında da sık görülür: WHERE date(olusturma_tarihi) = '2026-09-30' yerine WHERE olusturma_tarihi >= '2026-09-30' AND olusturma_tarihi < '2026-10-01' yazın.

2. Başı joker karakterli LIKE. WHERE ad LIKE 'Ahm%' bir B-tree indeksini (uygun ayarlarla) kullanabilir; ama WHERE ad LIKE '%met' kullanamaz. Rehberde "sonu -met ile biten soyadları" aramak gibidir. Metin içi arama için PostgreSQL'in tam metin arama özelliği ya da pg_trgm eklentisiyle trigram indeksleri gerekir.

3. Tür uyuşmazlığı. Sayısal bir sütunu metinle karşılaştırmak ya da tersini yapmak, bazı durumlarda indeksin kullanılmasını engelleyebilir. Parametre türlerinin sütun türüyle eşleştiğinden emin olun.

4. Güncel olmayan istatistikler. Veritabanı, indeksi kullanıp kullanmayacağına tablo hakkındaki istatistiklere bakarak karar verir. Büyük bir veri yüklemesinden sonra ANALYZE siparisler; çalıştırmak planlayıcının doğru kararı vermesine yardım eder. PostgreSQL bunu autovacuum ile otomatik de yapar.

İndekslerin bedeli

"Madem bu kadar hızlandırıyor, her sütuna indeks ekleyelim" demek cazip ama yanlış:

  • Yazma maliyeti: Her INSERT, UPDATE ve DELETE, ilgili tüm indekslerin de güncellenmesini gerektirir. On indeksli bir tablo, yazma işlemlerinde belirgin şekilde yavaşlar.
  • Disk ve bellek: İndeksler yer kaplar. Sık kullanılan indekslerin belleğe sığması performans için önemlidir.
  • Kullanılmayan indeksler: Zamanla eklenen ama hiçbir sorgunun kullanmadığı indeksler sadece yük getirir. PostgreSQL'de pg_stat_user_indexes görünümündeki idx_scan sütunu, bir indeksin kaç kez kullanıldığını gösterir.

Canlı sistemde indeks eklemek

Büyük bir tabloya normal CREATE INDEX ile indeks eklemek, işlem süresince tabloya yazmayı engeller. Canlı bir e-ticaret sitesinde bu, dakikalarca sipariş alınamaması demek olabilir. PostgreSQL'de bunun çözümü:

CREATE INDEX CONCURRENTLY idx_siparisler_musteri_id ON siparisler (musteri_id);

CONCURRENTLY seçeneği daha uzun sürer ama tabloya yazmayı engellemez. Bir işlem (transaction) bloğu içinde çalıştırılamadığını ve başarısız olursa geçersiz bir indeks bırakabileceğini unutmayın; böyle bir durumda indeksi silip yeniden oluşturmanız gerekir.

Hangi sorgular yavaş? Önce onu bulun

Hangi sorguya indeks gerektiğini tahminle değil, veriyle bulun. PostgreSQL'in pg_stat_statements eklentisi, en çok toplam süre harcayan sorguları listeler:

SELECT query, calls, round(total_exec_time) AS toplam_ms,
       round(mean_exec_time, 1) AS ortalama_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Çoğu zaman listenin başında, tek başına çok yavaş olmayan ama saniyede yüzlerce kez çalışan bir sorgu çıkar. Asıl kazanç oradadır.

Sıkça sorulan sorular

Birincil anahtara ayrıca indeks eklemem gerekir mi?

Hayır. PostgreSQL, birincil anahtar ve benzersiz (UNIQUE) kısıtlamalar için otomatik olarak indeks oluşturur. Ama yabancı anahtar sütunları için oluşturmaz; bunları kendiniz eklemelisiniz.

ORM kullanıyorum, indeksler beni ilgilendirir mi?

Evet. ORM'ler (Hibernate, Entity Framework, Prisma vb.) SQL'i sizin yerinize yazar ama indeksleri genellikle sizin tanımlamanızı bekler. ORM'in ürettiği sorguları loglarda görüp EXPLAIN ANALYZE ile incelemek iyi bir alışkanlıktır.

İndeks eklediğim hâlde veritabanı neden Seq Scan yapıyor?

Planlayıcı, indeksin bu sorgu için daha pahalı olacağına karar vermiş olabilir. Sorgu tablonun büyük bir bölümünü döndürüyorsa, tabloyu sırayla okumak gerçekten daha hızlıdır. Ya da yukarıdaki hatalardan biri (fonksiyonla sarma, tür uyuşmazlığı) indeksin kullanılmasını engelliyordur.

Veritabanım yavaş, daha güçlü sunucu mu almalıyım?

Önce indeksleri ve sorguları inceleyin. Eksik bir indeksin neden olduğu yavaşlığı donanımla çözmeye çalışmak, hem pahalı hem de geçicidir: veri büyümeye devam ettikçe sorun geri gelir.

Doğru indeks, bir uygulamanın büyüdükçe yavaşlaması ile büyüdükçe hızını koruması arasındaki fark olabilir. Uygulamanızın performans sorunlarını teşhis etmek ya da ölçeklenebilir bir altyapı kurmak isterseniz web uygulama geliştirme sayfamızdan bize ulaşabilirsiniz.

Kaynaklar

PaylaşLinkedInXWhatsApp
Bu konuda yardıma mı ihtiyacınız var?

Yazıda anlattıklarımızı kendi projenize uygulamak isterseniz birlikte bakalım.

Bize yazın
YE

EngerekTech’in kurucusu. İşletmeler için web, mobil ve kurumsal yazılım geliştiriyor; Angular, Spring Boot ve Flutter ile çalışıyor, KPSS Düello ve Kelime Kavanozu uygulamalarını geliştirdi. Blogda yapay zekâ araçlarını ve yazılım geliştirmeyi kendi projelerinde kullandığı haliyle anlatıyor.