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
- Slow query log açın ve en yavaş 10 sorguyu tespit edin
- EXPLAIN ile her sorguyu analiz edin
- N+1 sorunlarını eager loading ile çözün
- Eksik index'leri ekleyin — composite index'e öncelik verin
- Gerekiyorsa query cache veya Redis cache ekleyin
- Partitioning'i yalnızca 10M+ satır tablolar için düşünün