Bir müşterinin uygulamasında sayfa yükleme süresi 8 saniyeydi. Slow query log'u açtım ve tek bir sorgunun 3.2 saniye sürdüğünü gördüm. 45 dakikalık analizin ardından doğru bir composite index ekledik — sorgu 12 milisaniyeye düştü. Bu yazı, o süreçte kullandığım teknikleri sistematik biçimde anlatıyor.

Adım 1: Slow Query Log'u Etkinleştirin

Hangi sorguların yavaş olduğunu bilmeden optimizasyon yapamazsınız. MySQL'de yavaş sorgu log'unu etkinleştirin:

# my.cnf / my.ini
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1      # 1 saniyeden uzun sorguları logla
log_queries_not_using_indexes = 1

Adım 2: EXPLAIN ile Analiz

Yavaş sorguyu tespit ettikten sonra EXPLAIN komutuyla çalışma planını inceleyin:

EXPLAIN SELECT o.id, o.status, u.email
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'pending'
  AND o.created_at > '2026-01-01'
ORDER BY o.created_at DESC;

EXPLAIN Çıktısında Dikkat Edilecekler

  • type = ALL: Full table scan — kritik sorun, index ekleyin
  • type = index: Index taraması — daha iyi ama hâlâ yavaş olabilir
  • type = ref / range: İyi — index verimli kullanılıyor
  • rows: Taranan tahminî satır sayısı — ne kadar az, o kadar iyi
  • Extra = Using filesort: Disk sıralama — composite index ile çözebilirsiniz

Adım 3: Doğru Index Stratejisi

Index ekleme kör bir işlem değildir. WHERE, JOIN ve ORDER BY koşullarınıza göre seçici olun.

-- WHERE status AND ORDER BY created_at için composite index
ALTER TABLE orders
ADD INDEX idx_status_created (status, created_at DESC);

-- Covering index: sorgu tablo erişimi olmadan çalışır
ALTER TABLE orders
ADD INDEX idx_covering (status, created_at, id, user_id);

-- Partial index (MySQL 8+)
ALTER TABLE orders
ADD INDEX idx_pending (created_at)
WHERE status = 'pending';  -- sadece pending kayıtlar

N+1 Problemi ve Çözümü

ORM kullanan uygulamalarda en yaygın performans sorunudur. 100 sipariş çekerken her sipariş için ayrı kullanıcı sorgusu — 101 sorgu yerine 2 sorgu yeterlidir:

// YANLIŞ — N+1 sorunu
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->user->email; // Her döngüde SELECT
}

// DOĞRU — Eager loading
$orders = Order::with(['user:id,email'])->get();
foreach ($orders as $order) {
    echo $order->user->email; // Cached — ek sorgu yok
}

Partition Kullanımı

Milyonlarca satır içeren tablolarda partitioning önemli kazanımlar sağlar. Örneğin aylık partition ile sorgu yalnızca ilgili bölümü tarar:

ALTER TABLE orders
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
    PARTITION p202601 VALUES LESS THAN (202602),
    PARTITION p202602 VALUES LESS THAN (202603),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

Sonuç: Optimizasyon Önceliklendirmesi

  1. Slow query log açın ve en yavaş 10 sorguyu tespit edin
  2. EXPLAIN ile her sorguyu analiz edin
  3. N+1 sorunlarını eager loading ile çözün
  4. Eksik index'leri ekleyin — composite index'e öncelik verin
  5. Gerekiyorsa query cache veya Redis cache ekleyin
  6. Partitioning'i yalnızca 10M+ satır tablolar için düşünün