SQL PERFORMANS TEMELLERİ

Maroon SQL veritabanı silindirinin yanında metalik kronometre ile sorgu performansını sembolize eden temiz kompozisyon

Geliştirici makinesinde 2 saniyede dönen sorgu, üretim ortamında 3 dakika sürer ve sistemi yavaşlatır. "Çalışıyor" değil "hızlı çalışıyor" hedefi devreye girince SQL'in farklı bir disiplini başlıyor. Bu disiplinin adı performans.

Çoğu performans sorunu büyük veri ya da güçsüz sunucu meselesi değil — yanlış yazılmış sorgu. İyi indeks tasarımı ve birkaç temel pratiği bilen bir analist, milyonlarca satırlık tabloda saniyede sonuç alabilirken, aynı sorguyu yanlış yazan kişi sunucuyu yorabiliyor.

İndeks: Veriye Hızlı Erişim

SQL performansının temel kavramı indeks. Veritabanı tablolarındaki indeks, kitap arkasındaki indeks gibi: arananı bulmak için tüm sayfaları taramak yerine ilgili sayfaya gidiyor.

İndeks oluşturulur ama otomatik kullanılmaz; sorgunun yazım şekli indeksi tetiklemek zorunda. WHERE koşulu sol tarafta indeksli kolon olduğunda indeks çalışır:

-- İndeks çalışır
WHERE musteri_id = 1234

-- İndeks çalışmaz (sol tarafta fonksiyon)
WHERE YEAR(siparis_tarihi) = 2026

-- İndeks çalışır (sağ tarafta sabit aralık)
WHERE siparis_tarihi >= '2026-01-01'
 AND siparis_tarihi < '2027-01-01'

Pratik kural: indeksli kolonun üzerinde fonksiyon çalıştırma. Aksi halde indeks devre dışı kalıyor, full table scan yapılıyor.

SELECT * Kaçınılmalı

"Tüm kolonları getir" söz dizimi olan SELECT * üretim sorgularında problemli. Üç ana sorun yaratıyor:

  • Gereksiz veri çekiliyor — ağ trafiği, bellek, disk I/O
  • Tablo yapısı değiştiğinde sorgu beklenmedik şekilde davranıyor
  • Covering index avantajı kayboluyor (indeks tüm kolonları kapsamadığı için tabloya gitmek gerekiyor)

İhtiyaç duyulan kolonları açıkça yazmak başlangıçta yorucu görünür ama performans ve bakım açısından doğru tercih. Otomatik tamamlama ile bu yazımın yükü azaltılıyor.

Execution Plan Okumak

Performans optimizasyonunun en pratik aracı: execution plan. SQL Server Management Studio'da Ctrl+M, PostgreSQL'de EXPLAIN ANALYZE, MySQL'de EXPLAIN. Sorgunun nasıl çalışacağını adım adım gösteriyor.

Execution plan'da bakılması gereken sinyaller:

  • Table Scan / Index Scan: Tüm tablo veya indeks taranıyor. Büyük tablolarda problem.
  • Index Seek: Doğrudan ilgili satıra gidildi. İdeal durum.
  • Sort: Sonuç sıralanıyor. ORDER BY için indeks yok demek.
  • Hash Match: JOIN bir hash tablosu kurarak yapıldı. Çok büyük tablolarda RAM yiyor.
  • Estimated vs Actual rows: Tahmin ile gerçek arasında büyük fark = istatistik güncel değil

Execution plan'a bakmayı öğrenmek, SQL performansının kapısını açıyor. "Sorgu neden yavaş" sorusunun cevabı plan'da görünüyor.

JOIN Sırası ve İndeksler

Birden fazla tabloyu birleştiren JOIN sorgularında performans iki şeye bağlı: JOIN sırası ve JOIN kolonlarının indekslenmiş olması.

Veritabanı motoru çoğu zaman optimum sırayı kendi seçer — istatistikler güncelse. Güncel özellikler ve seçenekler Wikipedia'nın SQL sayfasında listelenir. Manuel sıralama gerekirse, küçük tablo dış JOIN'de, büyük tablo iç JOIN'de daha verimli. JOIN kolonu (genelde foreign key) indekslenmiş olmalı; aksi halde her satır için tarama yapılıyor.

Indeks ile ve indekssiz sorgu sürelerini iki yatay süre barı ile karşılaştıran Table Scan ve Index Seek performans kartı

Subquery vs CTE vs JOIN

Aynı sonucu üç farklı yapıda yazmak mümkün:

-- Subquery (genelde yavaş)
SELECT * FROM musteriler
WHERE musteri_id IN (
 SELECT musteri_id FROM siparisler
 WHERE tutar > 1000
);

-- JOIN ile aynı sonuç (genelde hızlı)
SELECT DISTINCT m.*
FROM musteriler m
INNER JOIN siparisler s ON s.musteri_id = m.musteri_id
WHERE s.tutar > 1000;

-- CTE (okunabilir ama performans benzeri)
WITH yuksek_siparis AS (
 SELECT DISTINCT musteri_id
 FROM siparisler
 WHERE tutar > 1000
)
SELECT m.*
FROM musteriler m
INNER JOIN yuksek_siparis y ON y.musteri_id = m.musteri_id;

Modern veritabanı motorları çoğu zaman subquery'yi JOIN'e otomatik dönüştürüyor; ancak garanti yok. EXISTS yapısı NOT IN ve IN'den genelde daha verimli; NULL davranışı da daha güvenli.

LIMIT / TOP / FETCH İlk Tercih

"İlk 10 müşteriyi getir" sorgusunda LIMIT (PostgreSQL/MySQL), TOP (SQL Server) veya FETCH FIRST (Oracle) kullanmak şart. Aksi halde tüm sonuç çekiliyor, sonra uygulama kodunda kesiliyor.

SELECT TOP 10 musteri_id, ad, ciro
FROM musteriler
ORDER BY ciro DESC;

ORDER BY ile LIMIT birleştiğinde, veritabanı sıralı bir indeks kullanabilirse sorgu hızlanıyor. ciro üzerinde indeks varsa bu sorgu O(log n) zamanda dönüyor; indeks yoksa tüm tablo sıralanması gerekiyor.

İstatistik Güncelliği

SQL Server, PostgreSQL ve Oracle gibi motorlar sorgu planını tablolardaki istatistiklere göre üretir. Bu istatistikler — satır sayısı, kolon dağılımı, ortalama değer — düzenli güncellenmezse motor yanlış kararlar verir.

Çoğu modern DB otomatik istatistik günceller ama büyük veri yüklemelerinden sonra manuel güncelleme gerekebilir:

-- SQL Server
UPDATE STATISTICS siparisler;

-- PostgreSQL
ANALYZE siparisler;

-- Oracle
EXEC DBMS_STATS.GATHER_TABLE_STATS('schema','siparisler');

Bir gün hızlı çalışan sorgu sonradan yavaşladıysa ve sorgu/veri değişmediyse, ilk şüpheli istatistik güncelliği.

Common Pitfalls

Performans sorununa yol açan tekrar tekrar görülen yanlışlar:

  • Indeksli kolonda fonksiyon kullanmak (WHERE YEAR(date) = 2026)
  • SELECT * yazıp tüm kolonları getirmek
  • OR yerine UNION ALL kullanmak gerektiğinde fark etmemek
  • Subquery'yi correlated subquery yaparak her satır için yeniden çalıştırmak
  • NOT IN yerine NOT EXISTS kullanmamak (NULL davranışı)
  • Cursor kullanmak — set-based düşünmek yerine satır bazlı
  • İndeks aşırı kullanımı (her kolon indeksli) — INSERT/UPDATE yavaşlıyor

Bu yanlışların farkında olmak performans kalitesini dramatik artırıyor. Çoğu durumda kod değişikliği bile gerektirmiyor; sadece farklı yazım. SQL performansını sistematik öğrenmek için indeks tasarımı, execution plan analizi ve sorgu optimizasyonunu birlikte ele alan bir kapsamlı SQL eğitimi bu üç ekseni aynı çatı altında işliyor.

Sıkça Sorulan Sorular

Bir sorgu neden geliştirme ortamında hızlı, üretimde yavaş?

Üç ana sebep: veri hacmi farklı (üretimde milyonlarca satır var, geliştirmede 10 bin), istatistikler farklı (motor farklı planlar üretiyor), eş zamanlı yük farklı (üretimde diğer sorgular kaynak çekiyor). Geliştirmede de gerçek hacme yakın veri ile test yapmak en güvenli yaklaşım.

İndeks eklemek her zaman performansı artırır mı?

Okuma için evet ama yazma için tam tersi. İndeks INSERT, UPDATE, DELETE işlemlerini yavaşlatır çünkü her değişiklikte indeks de güncellenmek zorunda. 'Her kolona indeks ekle' yanlış strateji; sadece sık kullanılan filtre ve JOIN kolonlarına indeks koymak optimum dengeyi sağlıyor.

SELECT * yerine kolon listesi yazmak neden önemli?

Üç sebep var. Gereksiz veri çekilmiyor — ağ, bellek, disk I/O azalıyor. Tablo yapısı değişince sorgu beklenmedik davranmıyor. Covering index avantajı korunuyor — indeks tüm kolonları kapsadığında tabloya hiç gitmeden sonuç dönüyor. Üretim sorgularında SELECT * neredeyse her zaman yanlış tercih.

Execution plan nasıl yorumlanır?

Sağdan sola, alttan üste okunur. Table Scan ve Hash Match gibi pahalı operatörler problem işareti. Index Seek ideal durumdur — doğrudan ilgili satıra gidildi. Estimated vs Actual rows arasındaki büyük fark istatistik güncelliği sorununu gösterir. Operatör başına maliyet yüzdesi en pahalı kısmı işaret eder.

Subquery mi JOIN mi daha hızlı?

Modern veritabanı motorları çoğu zaman ikisini de aynı plana çeviriyor ama garanti yok. Karmaşık subquery'lerde JOIN versiyonu daha okunabilir ve genelde daha tahmin edilebilir performans veriyor. EXISTS yapısı IN'den daha verimli; NULL davranışı açısından da daha güvenli.

Veritabanı istatistikleri ne zaman güncellenmeli?

Çoğu modern DB otomatik günceller ama büyük veri yüklemelerinden (ETL toplu insert) ve büyük silme/güncelleme işlemlerinden sonra manuel UPDATE STATISTICS veya ANALYZE faydalı. Performans aniden düştüğünde ilk kontrol edilecek şey istatistiklerin güncelliği oluyor.

Cursor kullanmaktan neden kaçınılmalı?

Cursor satır bazlı işlem yapar; SQL'in optimize ettiği set-based çalışmaya ters. 100 bin satırlık cursor işlemi, aynı işin set-based yazımıyla 50-100 kat daha yavaş kalıyor. Çoğu cursor senaryosu UPDATE FROM, CTE veya MERGE ile set-based çevrilebiliyor. Cursor sadece çok özel durumlarda — sıralı işlem zorunluluğu — kullanılıyor.