Şubat başında bir müşterimiz aradı; sesinden panik okunuyordu. Yönetim panelindeki satış raporu kırk saniyede açılıyormuş, muhasebe ekibi sabah raporunu alamıyormuş ve hosting firması "sunucuyu büyütelim" teklifiyle gelmiş. Bağlanıp baktık: üç milyon satırlık sipariş tablosu, raporun filtrelediği iki kolonda tek bir indeks yok. Bir bileşik indeks ekledik, sorgu kırk saniyeden üç yüz milisaniyeye indi. Sunucu aynı sunucu, kod aynı kod.
Bu hikâyenin ilginç tarafı istisna olmaması. Devraldığımız projelerde ilk baktığımız yer veritabanıdır, çünkü deneyim şunu söylüyor: uygulama yavaşsa suçlu on defadan dokuzunda veritabanı, veritabanı yavaşsa suçlu neredeyse her zaman eksik ya da yanlış kurgulanmış indekstir. Ve çoğu zaman bir günlük indeks çalışması, aylarca sürecek "optimizasyon projesinden" daha fazla hız kazandırır.
Kitabın arkasındaki dizin
İndeksin mantığını anlatırken hep aynı benzetmeyi kullanırım: kalın bir kitapta "circuit breaker" kelimesini arıyorsunuz. Ya sayfa sayfa çevirirsiniz — veritabanı dilinde buna full table scan deniyor — ya da kitabın arkasındaki dizine bakar, doğrudan 214. sayfaya gidersiniz. İndeks tam olarak o dizindir: motor, milyonlarca satırı taramak yerine sıralı bir yapı (çoğunlukla B-tree) üzerinden birkaç adımda hedefe iner.
Madalyonun öbür yüzü de var. Her indeks, yazma maliyeti demektir: tabloya INSERT ya da UPDATE geldiğinde ilgili indekslerin hepsi güncellenir. Yirmi indeksli bir tabloya kayıt atmak, dizinini her cümlede yeniden basan bir kitaba benzer. Bu yüzden "her kolona indeks at" yaklaşımı, indekssizlik kadar zararlıdır; sanat, okuma kazancıyla yazma maliyeti arasındaki dengeyi kurmakta.
Bir kademe ilerisi de var: sorgunun istediği bütün kolonlar indeksin içindeyse motor tabloya hiç gitmez, cevabı doğrudan indeksten döndürür. Buna covering index deniyor ve sık çalışan dar sorgularda — "şu kullanıcının son yirmi sipariş numarası ve tarihi" gibi — fark hissedilir düzeydedir. Her sorguya covering index açılmaz elbette; ama günde yüz binlerce kez koşan bir-iki kritik sorgu için düşünmeye değer.
Hangi kolon indeksi hak eder?
Basit bir saha kuralımız var: sorguların WHERE koşulunda, JOIN bağlantısında ve ORDER BY ifadesinde geçen kolonlar indeks adayıdır; SELECT listesinde ne yazdığının indeksleme kararıyla ilgisi yoktur. Bu kadar basit bir kuralın bu kadar sık yanlış uygulanması hâlâ şaşırtıyor beni — SELECT'te görünen on kolona indeks atılmış, WHERE'deki asıl filtre kolonu çıplak bırakılmış projeler görüyoruz.
Seçicilik de hesaba katılmalı. "Cinsiyet" gibi iki-üç değer alan bir kolona tek başına indeks atmanın getirisi azdır; motor yine yarı tabloyu okuyacaktır. Buna karşılık müşteri numarası, e-posta, sipariş kodu gibi neredeyse benzersiz kolonlar indeksin en verimli çalıştığı yerlerdir. Yazma yoğunluğu yüksek tablolarda (log, event, ölçüm verisi) ise her indeksi iki kez düşünün; oraya eklediğiniz her dizin, saniyedeki kayıt kapasitenizden yer.
ORDER BY da indeksin görev alanına girer, bu sık unutuluyor. Sıralama uygun bir indeksle karşılanamıyorsa motor sonuç kümesini bellekte (yetmezse diskte) sıralar; EXPLAIN çıktısında "filesort" olarak görünen budur. Küçük sonuç kümesinde önemsizdir, ama "son kayıtlardan yirmi tane" tipindeki sorgularda doğru indeks, sıralamayı bedavaya getirir: motor zaten sıralı yapıdan ilk yirmi satırı okur ve durur.
Bileşik indekste sıralamanın matematiği
Asıl performans sıçramaları genellikle bileşik (composite) indekslerden gelir ve burada kolon sırası her şeydir. Baştaki rapor örneğine dönelim: sorgu "durum = 'tamamlandı' AND tarih >= X" şeklinde filtreliyordu. (durum, tarih) sıralı bir indeks bu sorguya kelimenin tam anlamıyla uçar: motor önce durum dalına iner, sonra tarih aralığını sıralı biçimde okur. Aynı indeks, tek başına "tarih >= X" soran bir sorguya çalışmaz; çünkü telefon rehberinde soyadını bilmeden isimle arama yapamazsınız — rehber soyada göre sıralıdır.
Pratik kural şu: eşitlikle filtrelenen ve seçiciliği yüksek kolon öne, aralık taraması yapılan kolon arkaya. Bu tek cümle, sahada gördüğümüz bileşik indeks hatalarının çoğunu çözüyor.
İndeksi sessizce devre dışı bırakan yazımlar
En sinsi durum, indeksin var olup kullanılmamasıdır; kimse hata görmez, sadece her şey yavaştır. Klasik örnek fonksiyon kullanımı: WHERE YEAR(tarih) = 2024 yazdığınız anda motor, fonksiyonu her satıra uygulamak zorunda kalır ve indeks devre dışıdır. Aynı sorguyu aralık olarak yazın: tarih >= '2024-01-01' AND tarih < '2025-01-01' — indeks yeniden oyuna girer. LIKE '%kelime%' kalıbı da benzer; baştaki joker karakter, dizinin sıralı yapısını anlamsızlaştırır. Metin içinde arama gerçekten gerekiyorsa doğru araç FULLTEXT indeksi ya da Elasticsearch gibi ayrı bir arama motorudur.
Bir de tip uyuşmazlığı tuzağı var; bunu geçen yıl bir araç takip projesinde acı biçimde yaşadık. Plaka kolonu VARCHAR, uygulama sorguya sayı gönderiyor; MySQL her satırda örtük tip dönüşümü yapıyor ve indeks fiilen yok sayılıyordu. Tek karakterlik düzeltme — sorguya tırnak eklemek — sorgu süresini saniyelerden milisaniyelere indirdi. (Bu tür hatalar kod incelemesinde görünmez; ancak ölçümle yakalanır.)
ORM kullanan ekiplere ayrı bir not: kodunuzda SQL görünmemesi, SQL üretilmediği anlamına gelmez. ORM'in ürettiği sorguları hiç okumamış ekiplerde iki klasik bulguyla karşılaşıyoruz: döngü içinde tek tek sorgu atan N+1 deseni ve hiçbir indekse oturmayan, otomatik üretilmiş WHERE koşulları. Geliştirme ortamında yüz satırlık tabloyla her şey ışık hızındadır; sorunlar üç milyon satırla tanışınca başlar. Test verinizi gerçekçi boyuta getirmeden performans hakkında konuşmayın.
EXPLAIN okumadan dokunmayın
Peki hangi sorgunun sorunlu olduğunu nereden bileceksiniz? Sezgiyle değil. Sezgi bu işte kötü bir danışmandır; yavaş sanılan sorgu çoğu zaman masumdur, asıl suçlu günde on bin kez çalışan "hızlı" sorgudur.
İki araç yeter. Birincisi EXPLAIN: şüphelendiğiniz sorgunun başına ekleyin ve çıktıya bakın. "type: ALL" görüyorsanız tam tablo taraması var demektir; "rows" kolonu motorun kaç satır taramayı planladığını söyler. Milyonluk tabloda "rows: 2.8M" görüp de "ama indeksimiz var" diyorsanız, indeksiniz o sorguya uymuyor demektir. İkincisi yavaş sorgu logu: long_query_time değerini 1 saniyeye çekin, bir hafta veri biriktirin, sonra en sık çalışan ve toplamda en çok zaman yiyen sorgulardan başlayın. Bu iki aracın kullanımı yarım gün sürer; getirisi çoğu projede donanım yatırımından fazladır.
Son bir hatırlatma: indeks çalışması tek seferlik değildir. Uygulama evrilir, sorgular değişir, bir zamanlar hayati olan indeks yetim kalır — ama yazma maliyetini üretmeye devam eder. Yılda bir kez kullanılmayan indeksleri denetleyip temizlemek, yenilerini eklemek kadar meşru bir bakım kalemidir. Veritabanını canlı bir sistem gibi düşünün; bir kez ayarlanıp unutulan bir kutu gibi değil.
Kırk saniyelik rapor ekranıyla yaşamak zorunda değilsiniz. Benzer bir yavaşlık hikâyeniz varsa bize ulaşın; çoğu zaman teşhis bir günlük iş çıkıyor ve sunucu faturasına dokunmadan çözülüyor.