"Veritabanı yavaş" cümlesi bir teşhis değildir. Yavaşlığın kaynağı neredeyse her zaman birkaç belirli sorgudur ve bunlar ölçülerek bulunur. Sunucuya kaynak eklemek, indekssiz bir tam tablo taramasını yalnızca biraz daha hızlı yapar.
Yavaş sorgu günlüğünü açın
MySQL yapılandırmasına şu satırları ekleyip servisi yeniden başlatın:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
Bir gün çalıştırın, sonra mysqldumpslow veya pt-query-digest
ile özetleyin. Bu araçlar sorguları normalleştirip toplam süreye göre sıralar. Dikkat:
en uzun süren tek sorgu değil, toplam süresi en yüksek sorgu ailesi
önemlidir. 20 ms süren ama saniyede 200 kez çalışan bir sorgu, 2 saniyelik günlük bir
rapordan çok daha pahalıdır.
Günlüğü beklemeden bakmak
Bir gün beklemek istemiyorsanız sys şeması aynı bilgiyi anlık verir:
SELECT query, exec_count, total_latency, rows_examined_avg
FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 10;
rows_examined_avg sütununa özellikle bakın. Taranan satır sayısının dönen
satır sayısına oranı, indeks eksikliğinin en dürüst göstergesidir. On satır döndürmek için
yüz bin satır taranıyorsa aranacak başka bir sebep yoktur.
EXPLAIN çıktısını okuyun
Sorgunun başına EXPLAIN ekleyin. Bakılacak alanlar:
- type —
ALLtam tablo taraması demektir; küçük tablolar dışında kötüdür.ref,rangeveconstiyidir. - key — hangi indeksin kullanıldığı.
NULLise indeks kullanılmıyor. - rows — taranması beklenen satır sayısı. Sonuçta dönen satır sayısıyla arasındaki fark ne kadar büyükse, indeks o kadar yetersizdir.
- Extra —
Using filesortveUsing temporaryifadeleri sıralama ya da gruplama için geçici yapı kurulduğunu gösterir; genellikle doğru bileşik indeksle ortadan kalkar.
type değeri | Anlamı | Değerlendirme |
|---|---|---|
const, eq_ref | Birincil ya da tekil anahtarla tek satır | En iyi |
ref | İndeksle sınırlı sayıda satır | İyi |
range | İndeks üzerinde aralık taraması | Kabul edilebilir |
index | Tüm indeksin baştan sona taranması | Şüpheli, incelenmeli |
ALL | Tam tablo taraması | Küçük tablolar dışında kötü |
Somut bir örnek
200 bin satırlık bir sipariş tablosunda şu sorguyu düşünün:
SELECT id, tutar FROM siparis WHERE musteri_id = 42 ORDER BY tarih DESC LIMIT 10;
İndeks yokken EXPLAIN çıktısı type: ALL verir, rows
alanında tablonun tamamı görünür ve Extra alanında Using filesort
yazar — sıralama ayrıca yapılmak zorundadır. (musteri_id, tarih) bileşik
indeksi eklendiğinde type alanı ref'e döner, taranan satır sayısı
o müşterinin sipariş adedine iner ve filesort kaybolur, çünkü indeks zaten
tarihe göre sıralıdır. Tek bir ALTER TABLE ile sorgu, tablo büyüdükçe yavaşlayan
bir sorgu olmaktan çıkar.
İndeks eklerken üç kural
- Sıra önemlidir. Bileşik indekste sütun sırası soldan sağa çalışır.
(musteri_id, tarih)indeksi, yalnızmusteri_idile yapılan aramada da kullanılır; yalnıztarihile yapılanda kullanılmaz. - Sütunu sarmalamayın.
WHERE YEAR(tarih) = 2026yazarsanız indeks devre dışı kalır. Doğrusu aralık kullanmaktır:WHERE tarih >= '2026-01-01' AND tarih < '2027-01-01'. - Fazla indeks de maliyettir. Her indeks, yazma işlemlerini yavaşlatır
ve disk yer kaplar. Kullanılmayan indeksleri tespit edip kaldırın:
SELECT * FROM sys.schema_unused_indexes;sunucunun son açılışından beri hiç okunmamış indeksleri listeler. Birkaç haftalık kesintisiz çalışmadan sonra bu liste güvenilir hale gelir.
Kapsayan indeks
Sorgunun ihtiyaç duyduğu tüm sütunlar indeksin içindeyse veritabanı tabloya hiç
gitmez. SELECT durum, tutar FROM siparis WHERE musteri_id = 42 sorgusu için
(musteri_id, durum, tutar) indeksi, tablo satırlarını okumadan cevap verir.
Sık çalışan raporlarda bu yöntem birkaç kat hızlanma sağlar.
Sık görülen dört desen
SELECT *— ihtiyaç duyulmayan sütunları da taşır, kapsayan indeks olasılığını yok eder.- N+1 sorgusu — listedeki her satır için ayrı sorgu atılması. Uygulama tarafında tek sorguya toplanır; ORM kullanıyorsanız ilişkili verileri önden yükleyin.
- Büyük OFFSET —
LIMIT 20 OFFSET 100000ilk yüz bin satırı okuyup atar. Bunun yerine son görülen kimlikten devam eden sayfalama kullanın. LIKE '%kelime%'— baştaki joker indeksi kullanılamaz hale getirir. Metin araması için tam metin indeksi veya ayrı bir arama motoru gerekir.
Bellek ayarları
InnoDB tampon havuzu (innodb_buffer_pool_size), veri ve indeksleri bellekte
tutan asıl yapıdır. Yalnız veritabanı çalışan bir sunucuda toplam belleğin %60–70'i makul
bir başlangıçtır; web sunucusu ve PHP aynı makinedeyse bu oranı düşürün. Ayarı kör
büyütmeyin: takas alanına düşen bir veritabanı, küçük tampon havuzundan çok daha yavaştır.
Tampon havuzu isabet oranını ölçün. Sürekli diskten okuma yapılıyorsa ya havuz küçüktür ya da sorgular gereğinden fazla veri tarıyordur. İkinci ihtimali önce eleyin; indeks düzeltmesi bellek eklemekten hem ucuz hem kalıcıdır.
| Ayar | Makul başlangıç | Ne yapar |
|---|---|---|
innodb_buffer_pool_size | Adanmış sunucuda belleğin %60–70'i | Veri ve indeksleri bellekte tutar |
innodb_log_file_size | Tampon havuzunun dörtte biri | Yazma yoğun yükte disk baskısını düşürür |
innodb_flush_method | O_DIRECT | İşletim sistemiyle çift önbelleklemeyi önler |
max_connections | Gerçek eşzamanlılığın biraz üstü | Gereğinden yüksek değer bellek yer, hız getirmez |
İndeks ne zaman çözüm değildir?
Bazı yavaşlıklar indeksle düzelmez, çünkü sorun sorgunun kendisinde değildir:
- Sayfa başına onlarca sorgu. Her biri 2 ms sürse bile toplam 200 ms eder. Çözüm indeks değil, sonucu bellekte tutan bir önbellek katmanıdır; WordPress hızlandırma yazısındaki nesne önbelleği bölümü tam bu deseni anlatıyor.
- Yazma yoğun yük. Her yeni indeks, her
INSERTveUPDATEişlemine maliyet ekler. Günlük kaydı tutan tablolarda indeks sayısını asgaride tutun. - Raporlama sorguları. Milyonlarca satırı toplayan aylık ciro raporu, canlı tabloda çalıştırılmamalıdır. Gece hesaplanan bir özet tablo hem raporu anında açar hem canlı yükü rahatlatır.
- Yetersiz disk. Veri kümesi tampon havuzuna sığmıyorsa okuma hızını disk belirler. Böyle bir durumda NVMe'ye geçmek, indeks düzeltmesi kadar fark yaratır.
Uygulamalı örnek: 200 bin satırda indeksten önce ve sonra
Yukarıdaki sipariş sorgusunu bir deneme sunucusunda gerçekten çalıştırdık: Ubuntu 24.04 üzerinde MySQL 8.0, 200.000 satırlık bir siparis tablosu, 5.000 farklı müşteri. Ölçüm için EXPLAIN yerine EXPLAIN ANALYZE kullandık; ilki tahmin verir, ikincisi sorguyu çalıştırıp gerçek süreyi yazar.
EXPLAIN ANALYZE SELECT id, tutar FROM siparis
WHERE musteri_id = 42 ORDER BY tarih DESC LIMIT 10\G
İndeks yokken çıktının en alt satırı sorunu kendisi söylüyor:
-> Table scan on siparis (actual time=1.64..62.2 rows=200000 loops=1)
-> Sort: siparis.tarih DESC, limit input to 10 row(s) ... (actual time=70.5..70.5 rows=10)
Ekranda 10 satır görmek için veritabanı 200.000 satırı okumuş, eşleşen 37 satırı sıralamış: toplam yaklaşık 70 ms. Tek başına küçük görünen bu süre, sayfa başına birkaç kez ve saniyede onlarca ziyaretçiyle çarpıldığında işlemciyi tek başına doldurur. Aynı sorguyu rastgele müşterilerle 200 kez arka arkaya çalıştırmak 14,2 saniye sürdü.
Ardından sorgunun hem süzme hem sıralama sütununu kapsayan bileşik indeksi ekledik:
ALTER TABLE siparis ADD INDEX ix_musteri_tarih (musteri_id, tarih);
-> Index lookup on siparis using ix_musteri_tarih (musteri_id=42) (reverse)
(actual time=0.219..0.221 rows=10 loops=1)
Süre 70 ms'den 0,22 ms'ye indi; 200 sorguluk deneme 1,8 saniyeye düştü ve bu sürenin büyük kısmı artık her seferinde yeniden başlatılan istemci programının kendisiydi. EXPLAIN çıktısında type değeri ALL'dan ref'e döndü, Using filesort kayboldu ve yerine Backward index scan geldi: indeks zaten tarihe göre sıralı olduğu için ayrı bir sıralama adımına gerek kalmadı.
Aynı deneyde YEAR() tuzağı
Tarih sütununa ayrı bir indeks ekleyip iki sorguyu karşılaştırdık. WHERE YEAR(tarih) = 2026 ile planlayıcı indeksin tamamını taradı (type: index, 200.005 satır). Aynı koşulu tarih >= '2026-01-01' AND tarih < '2027-01-01' biçiminde yazınca plan range'e döndü ve yalnız ilgili aralık okundu. Sonuç aynıdır, iş yükü değildir.
Bir not daha: Ubuntu deposundan kurulan MySQL 8.0, innodb_buffer_pool_size değerini 128 MB ile getirir; 4 GB belleği olan deneme sunucumuzda da öyleydi. Veritabanınız bundan büyükse bu ayarı bellek ayarları bölümündeki ölçüye göre büyütmek, indekslerden sonraki ilk iştir.
Bakım işleri
- Büyük silme işlemlerinden sonra tabloyu yeniden düzenleyin; boşluk kendiliğinden geri verilmez.
- İstatistikleri güncel tutun; planlayıcı yanlış istatistikle yanlış indeksi seçer.
- Karakter kümesini
utf8mb4yapın; eskiutf8dört baytlık karakterleri saklayamaz.
Veritabanı yükü paylaşımlı paketin sınırlarını zorluyorsa, ayrılmış kaynak sunan VDS veya cloud sunucu tarafına geçmek bellek ayarlarını kendiniz belirlemenize izin verir. Ne kadar kaynak gerektiğini tahmin etmek yerine ölçmek için kaynak planlama yazısına bakın; önce sorguları düzeltip sonra ölçmek, gereksiz büyütmenin önüne geçer.
Sık sorulan sorular
EXPLAIN ile EXPLAIN ANALYZE arasındaki fark nedir?
EXPLAIN sorguyu çalıştırmadan planlayıcının tahminini gösterir. EXPLAIN ANALYZE (MySQL 8.0.18 ve sonrası) sorguyu gerçekten çalıştırır ve her adımın gerçek süresini ve okuduğu satır sayısını yazar. Tahmin ile gerçek arasında büyük fark görürseniz tablo istatistikleri eskimiş olabilir; ANALYZE TABLE çalıştırın.
Büyük bir tabloya indeks eklemek siteyi kilitler mi?
InnoDB çoğu indeks eklemeyi tabloyu yazmaya kapatmadan yapar, ama işlem disk ve işlemci tüketir ve büyük tablolarda dakikalar sürebilir. Yoğun saatlerden kaçının, öncesinde yedek alın ve diskte tablonun kendisi kadar boş alan olduğundan emin olun.
Her sütuna indeks eklesem sorun çözülür mü?
Hayır. Her indeks her yazma işlemine maliyet ekler ve bellekte yer kaplar. İndeksi yavaş sorgu günlüğünde gerçekten görünen sorgulara göre ekleyin; bileşik indekste sütun sırası sorgunun WHERE ve ORDER BY yapısına uymalıdır.
Paylaşımlı hostingde yavaş sorgu günlüğünü açabilir miyim?
Genellikle hayır; bu ayar sunucu genelidir. Paylaşımlı pakette EXPLAIN sorgularını phpMyAdmin üzerinden çalıştırabilir ve indeksleri yine ekleyebilirsiniz. Günlük ve bellek ayarlarına erişim gerekiyorsa kök erişimli bir sunucuya ihtiyaç vardır.
- #mysql
- #indeks
- #veritabanı
Veritabanı ayarlarına kök erişimiyle dokunmanız mı gerekiyor? VDS paketlerimizde işlemci ve bellek size ayrılmıştır; disk NVMe'dir, MySQL yapılandırması tamamen sizdedir.
VDS paketlerine bakın