Veritabanı Yönetimi

    SQL Index Tasarımı: Ne Zaman Hangi Kolona Index Açılır

    Hangi kolona index açılacağını seçicilik, bileşik sıra ve yazma maliyeti üzerinden karar verme rehberi.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    Bir sorgu yavaşladığında ilk refleks genelde "buraya bir index atalım" olur. Birkaç ay sonra tabloda on iki tane index birikir, INSERT işlemleri sürünmeye başlar, disk şişer ve o yavaş sorgu hâlâ yavaştır. SQL index tasarımı bu yüzden "index açmak" değil, hangi kolona neden index açıldığını gerekçelendirebilmek demektir. İyi bir index tablodaki milyonlarca satırı okumadan doğru sayfaya gitmeni sağlar; kötü bir index ise hem yer kaplar hem her yazma işlemine ek yük bindirir hem de optimizer'ı yanıltır.

    Bu rehberde index'in altında yatan B+Tree yapısından başlayıp, bir kolonun index'e değip değmediğine nasıl karar verileceğini, bileşik (composite) index'lerde kolon sırasının neden her şeyi belirlediğini, covering index'in ne zaman devreye girdiğini ve gerçek hayatta index'i sessizce devre dışı bırakan sorgu kalıplarını anlatacağım. Örnekler MySQL/MariaDB InnoDB üzerinden ama mantık PostgreSQL dahil çoğu ilişkisel veritabanında aynıdır.

    Index Neden Hızlandırır: B+Tree'nin Kısa Hikâyesi#

    InnoDB'de her index bir B+Tree'dir. Ağacın kök düğümünden yapraklara doğru inersin ve her seviyede yalnızca birkaç karşılaştırma yaparak aradığın aralığa ulaşırsın. Milyon satırlık bir tabloda ağacın derinliği tipik olarak üç-dört seviyedir; yani index'siz halde 1.000.000 satır okuman gereken bir arama, index'le birkaç sayfa okumaya iner. Fark logaritmik olduğu için tablo büyüdükçe index'in kazancı da büyür — 10 bin satırda fark etmeyen bir eksik index, 10 milyon satırda sunucuyu dize getirir.

    InnoDB'de ayrıca çok kritik bir ayrıntı var: tablonun kendisi de bir B+Tree'dir ve birincil anahtara göre kümelenmiştir (clustered index). Yani satırların fiziksel sırası primary key sırasıdır. İkincil (secondary) index'lerin yapraklarında satırın tamamı değil, birincil anahtar değeri durur. Bunun pratik sonucu şudur: ikincil bir index üzerinden bulduğun satırın diğer kolonlarına erişmek için ikinci bir arama (bookmark lookup) gerekir. Bu yüzden UUID gibi rastgele ve uzun bir birincil anahtar seçmek, tablodaki her ikincil index'i şişirir ve yavaşlatır.

    -- Kümelenmiş index'i doğru kurmak, sonraki tüm index'lerin maliyetini belirler
    CREATE TABLE siparis (
      id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,  -- artan, kısa, kümelenmeye uygun
      musteri_id  INT UNSIGNED NOT NULL,
      durum       TINYINT NOT NULL,
      tutar       DECIMAL(10,2) NOT NULL,
      olusturma   DATETIME NOT NULL,
      PRIMARY KEY (id),
      KEY ix_musteri_tarih (musteri_id, olusturma)
    ) ENGINE=InnoDB;
    

    Hangi Kolona Index Açılır: Karar Kriterleri#

    Index kararının merkezinde seçicilik (selectivity) vardır: bir kolondaki farklı değer sayısının toplam satır sayısına oranı. Oran 1'e yaklaştıkça kolon seçicidir, 0'a yaklaştıkça değildir. email kolonu neredeyse tekildir, seçiciliği 1'e yakındır ve mükemmel bir index adayıdır. aktif gibi iki değerli bir kolonun seçiciliği 0,0000x mertebesindedir; tek başına index açmak neredeyse hiçbir işe yaramaz, çünkü optimizer "zaten satırların yarısını okuyacağım, index'e uğrayıp sonra tabloya dönmektense doğrudan tam tarama yaparım" der ve haklıdır.

    Seçiciliği tahmin etmeyi bırak, ölç:

    -- Kolon başına seçicilik: 1'e ne kadar yakınsa o kadar iyi aday
    SELECT
      COUNT(DISTINCT musteri_id) / COUNT(*) AS musteri_sec,
      COUNT(DISTINCT durum)      / COUNT(*) AS durum_sec,
      COUNT(DISTINCT DATE(olusturma)) / COUNT(*) AS gun_sec
    FROM siparis;
    

    Seçicilik tek kriter değil. Bir kolonun index'e değmesi için aynı zamanda sorgularda gerçekten filtre olarak kullanılıyor olması gerekir. Uygulamanın hiç WHERE tutar = ? yazmadığı bir kolona seçicilik yüksek diye index açmak sadece yazma maliyeti üretir. Karar verirken şu tabloyu kullanabilirsin:

    Kolon tipi / kullanımIndex açılır mıGerekçe
    Yabancı anahtar (musteri_id)Evet, neredeyse her zamanJOIN ve satır silmede zorunlu; InnoDB FK için zaten ister
    WHERE içinde eşitlikle sık geçen seçici kolonEvetKlasik index kazancı
    ORDER BY / GROUP BY kolonuGenelde evetFilesort ve geçici tabloyu ortadan kaldırır
    Düşük kardinaliteli bayrak (aktif, durum)Tek başına hayırBileşik index'in sonraki kolonu olarak değerli
    LIKE '%kelime%' ile aranan metinHayırÖne joker gelirse B+Tree kullanılamaz; full-text gerekir
    Sadece SELECT listesinde geçen kolonHayırFiltrelemiyorsa index'e ihtiyaç yok

    Metin içinde arama yapıyorsan B+Tree index'i seni kurtarmaz; bunun doğru aracı ayrı bir yapıdır ve MySQL full-text arama yazısında ayrıntısıyla anlatılıyor.

    Bileşik Index ve Soldan Sağa Kuralı#

    Bileşik index, birden fazla kolonu tek bir ağaçta sıralar. Kritik nokta şudur: sıralama soldan sağa yapılır, tıpkı bir telefon rehberinde önce soyada sonra ada göre sıralamak gibi. KEY (a, b, c) index'i şu filtrelerde kullanılabilir: sadece a; a ve b; a, b ve c. Ama sadece b, ya da sadece b ve c için kullanılamaz — çünkü rehberde soyadı bilmeden adı aramaya benzer.

    Bir başka önemli kural aralık (range) koşullarıyla ilgilidir: index, ilk aralık koşuluna kadar tam verimle çalışır, ondan sonraki kolonlar artık sadece filtreleme için okunur, arama için değil. Yani WHERE musteri_id = 5 AND olusturma > '2026-01-01' AND durum = 2 sorgusunda (musteri_id, olusturma, durum) sırası, durum kolonundan verim alamaz. Doğru sıra (musteri_id, durum, olusturma) olur: iki eşitlik önce, aralık en sonda.

    -- Yanlış: aralık ortada kaldığı için durum kolonu aramada kullanılamaz
    KEY ix_kotu (musteri_id, olusturma, durum);
    
    -- Doğru: eşitlikler önce, aralık en sonda
    KEY ix_iyi  (musteri_id, durum, olusturma);
    

    Pratik sıralama kuralını üç maddede özetleyebilirim:

    1. Eşitlikle (=, IN) filtrelenen kolonları en başa koy.
    2. Aralık (>, <, BETWEEN, LIKE 'abc%') koşulunu en sona koy.
    3. ORDER BY kolonunu, aralıktan hemen sonra değil, aralık yerine düşünmeye çalış; sıralama ve aralık aynı index'te aynı anda verim veremez.

    Bir index'in gerçekten kullanılıp kullanılmadığını tahmin etmek yerine EXPLAIN ile kanıtlamalısın; bu çıktının nasıl okunacağını MySQL EXPLAIN ile sorgu analizi yazısında satır satır açıkladım.

    Covering Index: Tabloya Hiç Dönmemek#

    Bir sorgunun ihtiyaç duyduğu tüm kolonlar index'in içinde varsa, veritabanı satırın kendisine hiç gitmez; cevabı doğrudan index yapraklarından üretir. Buna covering index denir ve EXPLAIN çıktısında Extra: Using index satırıyla kendini belli eder. Bookmark lookup adımını tamamen ortadan kaldırdığı için tipik olarak birkaç kat hızlanma sağlar.

    -- Sorgu sadece bu üç kolona dokunuyor
    SELECT musteri_id, olusturma, tutar
    FROM siparis
    WHERE musteri_id = 42 AND olusturma >= '2026-01-01';
    
    -- tutar kolonunu index'e ekleyince sorgu tabloya hiç uğramaz
    ALTER TABLE siparis DROP INDEX ix_musteri_tarih,
      ADD KEY ix_musteri_tarih_tutar (musteri_id, olusturma, tutar);
    

    Covering index'i her sorgu için kurmaya kalkma; ekstra kolon index'i genişletir, belleğe daha az sayfa sığar. Doğru kullanım, gerçekten sık çalışan ve yüksek maliyetli birkaç sorgu için hedefli biçimde yapmaktır. Özellikle sayfalama sorgularında (ORDER BY olusturma DESC LIMIT 20) covering index, filesort'u ve rastgele disk okumasını birlikte ortadan kaldırdığı için kazanç dramatik olur.

    Index'in Faturası: Yazma Maliyeti, Bellek ve Disk#

    Index bedava değildir. Her INSERT, UPDATE ve DELETE, tabloya ek olarak her index ağacını da güncellemek zorundadır. Sekiz index'i olan bir tabloya satır eklemek, tek index'i olan tabloya eklemekten belirgin biçimde pahalıdır. Yoğun yazma alan tablolarda (log, olay kaydı, kuyruk tabloları) index sayısını mümkün olduğunca düşük tutmak, okuma tarafında birkaç milisaniye kaybetmeye değer.

    İkinci fatura bellektir. InnoDB index sayfalarını da buffer pool'da tutar; gereksiz index'ler sıcak veriyi bellekten dışarı iter ve dolaylı olarak tüm sorguları yavaşlatır. Index'lerin ne kadar yer kapladığını görmek için:

    -- Tablo ve index boyutlarını MB cinsinden listele
    SELECT table_name,
           ROUND(data_length/1024/1024)  AS veri_mb,
           ROUND(index_length/1024/1024) AS index_mb
    FROM information_schema.tables
    WHERE table_schema = DATABASE()
    ORDER BY index_length DESC
    LIMIT 10;
    

    Index toplamı veri boyutunu geçmeye başladıysa bu bir alarm zilidir; büyük ihtimalle yinelenen veya hiç kullanılmayan index'lerin var. Bellek tarafındaki dengeyi InnoDB buffer pool ayarı yazısında ayrıca ele aldım.

    Kullanılmayan ve Yinelenen Index'leri Bulmak#

    (a) index'i varken bir de (a, b) index'i açtıysan, (a) gereksizdir: soldan sağa kuralı sayesinde geniş index dar olanın işini zaten yapar. Bu tür fazlalıklar zamanla sessizce birikir. Performance Schema açıksa hiç kullanılmayan index'leri doğrudan listeleyebilirsin:

    -- Hiç okunmamış ikincil index'ler (sunucu bir süredir ayakta olmalı)
    SELECT object_schema, object_name, index_name
    FROM performance_schema.table_io_waits_summary_by_index_usage
    WHERE index_name IS NOT NULL
      AND index_name <> 'PRIMARY'
      AND count_star = 0
      AND object_schema NOT IN ('mysql','performance_schema','sys')
    ORDER BY object_schema, object_name;
    

    Bir index'i silmeden önce iki şeyi doğrula: sunucunun yeterince uzun süredir çalışıyor olması (aylık çalışan bir rapor sorgusu o index'i kullanıyor olabilir) ve index'in bir tekillik kısıtı (UNIQUE) taşımaması. Silme işlemini büyük tablolarda ALGORITHM=INPLACE ile yapmak kilitlenmeyi azaltır:

    ALTER TABLE siparis DROP INDEX ix_eski, ALGORITHM=INPLACE, LOCK=NONE;
    

    Index'i Sessizce Devre Dışı Bırakan Kalıplar#

    En sinir bozucu durum, index'in var olduğu ama kullanılmadığı durumdur. Bunun neredeyse her zaman birkaç klasik sebebi olur. Birincisi kolonun üzerine fonksiyon uygulamak: WHERE DATE(olusturma) = '2026-08-25' yazdığında index kullanılamaz, çünkü ağaçta DATE(olusturma) değil olusturma sıralıdır. Doğrusu aralık yazmaktır:

    -- Index kullanılmaz
    WHERE DATE(olusturma) = '2026-08-25'
    
    -- Index kullanılır
    WHERE olusturma >= '2026-08-25 00:00:00'
      AND olusturma <  '2026-08-26 00:00:00'
    

    İkinci klasik hata tip uyumsuzluğudur. VARCHAR bir kolona sayı ile karşılaştırma yaparsan MySQL örtük dönüşüm uygular ve index devre dışı kalır. WHERE telefon = 5551234567 yerine WHERE telefon = '5551234567' yazmak tek başına sorguyu saniyelerden milisaniyelere indirebilir. Aynı tuzak JOIN kolonlarının farklı karakter setlerine (utf8mb4 ve latin1) sahip olduğu durumlarda da kurulur.

    Üçüncüsü öne joker konan LIKE kalıbıdır: LIKE '%ankara%' index kullanamaz, LIKE 'ankara%' kullanır. Dördüncüsü ise OR ile bağlanmış farklı kolonlardır; optimizer çoğu zaman tam tarama seçer, UNION ALL ile iki ayrı index'li sorguya bölmek daha hızlı olur. Bu kalıpları yakalamanın en pratik yolu yavaş sorgu günlüğünü açık tutmaktır; yöntemi MySQL yavaş sorgu bulma yazısında adım adım anlattım. Ayrıca depolama motorunun kendisi de belirleyicidir: MyISAM ile InnoDB'nin index davranışı aynı değildir, farkları InnoDB ve MyISAM karşılaştırması yazısında bulabilirsin.

    Sıkça Sorulan Sorular#

    Bir tabloda en fazla kaç index olmalı#

    Kesin bir sayı yok ama pratikte çoğu OLTP tablosu beş-altı index'le rahatça idare eder. Bunun üzerine çıkıyorsan büyük ihtimalle yinelenen ya da hiç kullanılmayan index'lerin vardır. Karar ölçütün sayı değil, kanıt olmalı: her index'i, onu kullanan gerçek bir sorguyla eşleştirebiliyor musun? Eşleştiremiyorsan o index sadece yazma maliyeti üretiyordur.

    Index açmak tabloyu kilitler mi#

    MySQL 5.6 ve sonrasında InnoDB'de çoğu index ekleme işlemi ALGORITHM=INPLACE, LOCK=NONE ile çevrimiçi yapılabilir; bu sırada okuma ve yazma devam eder. Yine de işlem sonunda kısa bir metadata kilidi alınır ve uzun süren bir işlem varsa beklemek zorunda kalır. Büyük tablolarda index eklemeyi trafiğin düşük olduğu saate almak, bir de öncesinde replikasyon gecikmesini kontrol etmek en güvenli yaklaşımdır.

    Primary key olarak UUID kullanmak neden sakıncalı#

    InnoDB tabloyu birincil anahtara göre fiziksel olarak sıralı tuttuğu için, rastgele üretilen bir UUID her ekleme işlemini ağacın rastgele bir yerine düşürür ve sayfa bölünmelerine yol açar. Ayrıca UUID uzun olduğundan tablodaki her ikincil index'in yaprağında bu uzun değer tekrar tekrar saklanır. Uygulama düzeyinde UUID gerekiyorsa, birincil anahtar olarak artan bir tamsayı kullanıp UUID'yi ayrı bir UNIQUE kolonda tutmak çok daha verimlidir.

    Index açtım ama sorgu hâlâ yavaş, ne yapmalıyım#

    Önce index'in gerçekten kullanıldığını EXPLAIN ile doğrula; type: ALL görüyorsan index devrede değildir. Kullanılmıyorsa sebep genelde kolona uygulanan bir fonksiyon, tip uyumsuzluğu ya da bileşik index'te yanlış kolon sırasıdır. Kullanılıyor ama yine yavaşsa, muhtemelen index çok fazla satır eşleştiriyordur; bu durumda seçiciliği artıracak ek bir kolon eklemek veya covering index'e çevirmek işe yarar.

    UNIQUE index normal index'ten daha mı hızlıdır#

    Okuma tarafında ikisinin B+Tree yapısı aynıdır, dolayısıyla arama maliyeti benzerdir. Farkı optimizer yaratır: UNIQUE bir index'te eşitlik araması en fazla bir satır döneceği için plan daha kesin ve genelde daha iyi olur. Buna karşılık yazma tarafında UNIQUE her eklemede tekillik kontrolü yapar, yani hafif bir ek maliyeti vardır. Veri gerçekten tekilse UNIQUE kullanmak hem doğruluk hem plan kalitesi açısından tercih edilir.

    Index'leri düzenli olarak yeniden oluşturmam gerekir mi#

    InnoDB'de rutin bir yeniden oluşturma ihtiyacı yoktur; ağaç kendini dengeler. Ancak çok sayıda silme ve güncellemeden sonra sayfalarda boşluk (fragmentation) birikebilir ve tablo diskte gereğinden fazla yer kaplayabilir. Böyle bir durumda OPTIMIZE TABLE ya da ALTER TABLE ... ENGINE=InnoDB ile tabloyu yeniden inşa edebilirsin. Bu işlem büyük tablolarda uzun sürdüğü için bakım penceresinde ve yedeğin varken yapılmalıdır.

    İstatistikler yanlışsa optimizer index'i atlar mı#

    Evet, bu oldukça yaygın bir durumdur. Optimizer kararını index istatistiklerine göre verir ve bu istatistikler örnekleme ile üretildiği için bazen gerçeği yansıtmaz. ANALYZE TABLE tablo_adi komutu istatistikleri tazeler ve çoğu zaman plan anında düzelir. Toplu veri yükledikten veya büyük bir silme işleminden sonra ANALYZE TABLE çalıştırmak iyi bir alışkanlıktır.

    Kapanış#

    Index tasarımını kafanda birkaç sağlam alışkanlığa indirgeyebilirsin: her index'i onu kullanan gerçek bir sorguyla gerekçelendir, bileşik index'lerde eşitlikleri başa aralıkları sona koy, kolonun üzerine fonksiyon uygulayarak index'i boşa çıkarma ve düzenli aralıklarla kullanılmayan index'leri temizle. Karar verirken tahmin etme; seçiciliği ölç, planı EXPLAIN ile kanıtla, boyutu information_schema üzerinden gör. Index sayısını azaltmak çoğu zaman index eklemekten daha büyük kazanç sağlar.

    Veritabanı sunucusunun altındaki donanım da bu tabloya dahildir: index'lerin belleğe sığması, disk okumasının hızlı olması ve CPU'nun sıkışmaması işin yarısıdır. Kendi MySQL sunucunu yönetmek istersen tam root erişimli VDS ve sanal sunucu paketlerimiz uygun bir başlangıç olur; yükün arttığı noktada bulut sunucu ile kaynak esnetebilirsin. Ayar, izleme ve bakım işini üstlenmemizi isterseniz sunucu yönetimi hizmetimiz veritabanı optimizasyonunu da kapsıyor.

    MySQLIndexPerformans

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.