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.

İç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
LIMITile 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,UPDATEveDELETE, 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_indexesgörünümündekiidx_scansü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
PostgreSQL DocumentationChapter 11. IndexesChapter 11. Indexes Table of Contents 11.1. Introduction 11.2. Index Types 11.2.1. B-Tree 11.2.2. Hash 11.2.3. GiST 11.2.4. SP-GiST 11.2.5. GIN 11.2.6. …
PostgreSQL Documentation14.1. Using EXPLAIN14.1. Using EXPLAIN # 14.1.1. EXPLAIN Basics 14.1.2. EXPLAIN ANALYZE 14.1.3. Caveats PostgreSQL devises a query plan for each query it …
PostgreSQL Documentation11.3. Multicolumn Indexes11.3. Multicolumn Indexes # An index can be defined on more than one column of a table. For example, if you …
PostgreSQL Documentation11.7. Indexes on Expressions11.7. Indexes on Expressions # An index column need not be just a column of the underlying table, but can be …
PostgreSQL DocumentationCREATE INDEXCREATE INDEX CREATE INDEX — define a new index Synopsis CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ …
PostgreSQL DocumentationF.32. pg_stat_statements — track statistics of SQL planning and executionF.32. pg_stat_statements — track statistics of SQL planning and execution # F.32.1. The pg_stat_statements View F.32.2. The pg_stat_statements_info View F.32.3. Functions …

