Veritabanı Yönetimi

    MongoDB Index ve Sorgu Optimizasyonu

    MongoDB'de explain çıktısını okuyup doğru index tasarlamanın pratik yöntemleri.

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

    MongoDB index ve sorgu optimizasyonu konusu genellikle çok geç gündeme gelir: koleksiyonda birkaç bin belge varken her şey anında döner, uygulama hızlıdır, kimse index'e bakmaz. Belge sayısı yüz bini geçtiğinde ise aynı sorgu birden saniyeler sürmeye başlar, işlemci tavan yapar ve "MongoDB yavaşladı" cümlesi kurulur. Oysa yavaşlayan MongoDB değildir; index'siz bir sorgu, koleksiyondaki her belgeyi tek tek okumak zorunda kalan bir tarayıcıya dönüşmüştür.

    Bu rehberde bir sorgunun nerede ve neden yavaşladığını explain çıktısıyla nasıl teşhis edeceğinizi, hangi index türünün hangi soruna çare olduğunu, bileşik index'lerde alan sırasının neden hayati olduğunu ve kapsayan sorgularla diske hiç inmeden nasıl sonuç döneceğinizi anlatacağım. Ayrıca yavaş sorguları kendiliğinden yakalayan profilleyiciyi kuracak, gereksiz index'lerin yazma performansına verdiği zararı ölçecek ve sahada en sık gördüğüm tasarım hatalarını tek tek göstereceğim. Örnekler mongosh üzerinden verilmiştir; MongoDB kurulumunu henüz yapmadıysanız MongoDB kurulumu yazısıyla başlayabilirsiniz.

    Bir Sorgunun Nerede Yavaşladığını Bulmak#

    Optimizasyona tahminle başlamayın; MongoDB size sorgu planını olduğu gibi verir. explain("executionStats"), sorguyu gerçekten çalıştırıp kaç belge okuduğunu, kaç index anahtarına baktığını ve kaç milisaniye harcadığını raporlar.

    // Örnek koleksiyon: siparisler
    db.siparisler.find({ musteriId: 4821, durum: "onaylandi" })
      .sort({ olusturma: -1 })
      .explain("executionStats")
    

    Çıktının içinde bakmanız gereken beş sayı var. Bunları ezberleyin, teşhisin yüzde sekseni burada biter:

    AlanAnlamıİdeal durum
    nReturnedSorgunun döndürdüğü belge sayısıİhtiyacınız olan sayı
    totalKeysExaminedTaranan index anahtarı sayısınReturned'a yakın
    totalDocsExaminedOkunan belge sayısınReturned'a yakın, ideali 0
    executionTimeMillisToplam süreMümkün olduğunca düşük
    winningPlan.stageKullanılan erişim yöntemiIXSCAN, asla COLLSCAN

    Altın oran şudur: totalDocsExamined / nReturned değeri 1'e ne kadar yakınsa sorgunuz o kadar iyidir. Bu oran 100 ise, döndürdüğünüz her belge için 100 belge okumuşsunuz demektir; index ya yoktur ya da yanlış tasarlanmıştır. Aşağıda tipik bir kötü plan görüyorsunuz:

    {
      executionStats: {
        nReturned: 12,
        executionTimeMillis: 1840,
        totalKeysExamined: 0,        // hiç index kullanılmamış
        totalDocsExamined: 486213,   // tüm koleksiyon taranmış
        executionStages: { stage: 'COLLSCAN', direction: 'forward' }
      }
    }
    

    COLLSCAN kelimesini gördüğünüz an durun: bu, koleksiyon taraması demektir ve belge sayısıyla doğru orantılı olarak yavaşlar. Bir başka kırmızı bayrak da SORT aşamasıdır. Sıralama index'ten karşılanamıyorsa MongoDB sonuçları bellekte sıralar ve bunun sert bir sınırı vardır; sınır aşıldığında sorgu Sort exceeded memory limit hatasıyla tamamen başarısız olur. Yani sıralamayı index'e taşımak sadece hız değil, kararlılık meselesidir.

    Aynı sorguyu index ekledikten sonra tekrar çalıştırdığınızda görmek istediğiniz tablo şudur: stage alanında IXSCAN, totalKeysExamined ile nReturned birbirine yakın, executionTimeMillis birkaç milisaniye.

    Index Türleri ve Ne Zaman Hangisi#

    MongoDB'de tek bir "index" yoktur; sorunuza göre farklı türler vardır. Aşağıdaki tablo, hangi ihtiyaç için hangisine uzanmanız gerektiğini özetliyor.

    TürNe içinÖrnek
    Tek alanBasit eşitlik ve aralık sorguları{ email: 1 }
    BileşikBirden fazla alanla filtre ve sıralama{ musteriId: 1, olusturma: -1 }
    Çok anahtarlıDizi alanları (otomatik oluşur){ etiketler: 1 }
    BenzersizTekrarı engelleme{ email: 1 }, { unique: true }
    KısmiYalnızca belgelerin bir alt kümesipartialFilterExpression
    SeyrekAlanı olmayan belgeleri dışarıda bırakma{ sparse: true }
    TTLBelgeyi süre sonunda otomatik silmeexpireAfterSeconds
    MetinSerbest metin arama{ aciklama: "text" }
    HashParçalama anahtarı için eşit dağılım{ _id: "hashed" }

    Kısmi index'ler pratikte en çok değeri veren ama en az kullanılan türdür. Örneğin siparişlerin yalnızca yüzde beşi "bekliyor" durumundaysa, sadece o belgeleri kapsayan bir index hem çok daha küçük olur hem de bellekte kalır:

    // Yalnızca bekleyen siparişleri kapsayan index
    db.siparisler.createIndex(
      { olusturma: -1 },
      {
        name: "bekleyen_olusturma",
        partialFilterExpression: { durum: "bekliyor" }
      }
    )
    

    Dikkat: kısmi index'in kullanılabilmesi için sorgunuzun da aynı koşulu içermesi gerekir. find({ durum: "bekliyor" }) yazarsanız index kullanılır; find({}) yazıp sadece sıralarsanız kullanılmaz, çünkü MongoDB index'in tüm belgeleri kapsamadığını bilir.

    TTL index'leri ise oturum, günlük ve geçici kayıt koleksiyonlarında elle temizlik yazmaktan kurtarır:

    // olusturma alanından 30 gün sonra belgeleri otomatik sil
    db.oturumlar.createIndex({ olusturma: 1 }, { expireAfterSeconds: 2592000 })
    

    TTL silicisinin arka planda yaklaşık altmış saniyede bir çalıştığını unutmayın; belgeler süre dolar dolmaz değil, bir sonraki tarama turunda silinir. Ayrıca yalnızca tarih tipindeki alanlarda çalışır, sayısal zaman damgasında çalışmaz.

    Bileşik Index Tasarımı ve ESR Kuralı#

    Bileşik index'lerde alanların sırası her şeydir. MongoDB bir bileşik index'i soldan sağa doğru kullanabilir; yani { a: 1, b: 1, c: 1 } index'i a ile, a + b ile ve a + b + c ile yapılan sorgulara hizmet eder, ama tek başına b ya da c sorgusuna hizmet edemez. Bu özelliğe "önek kuralı" denir ve doğru anlaşıldığında index sayınızı yarıya indirir.

    Sırayı belirlerken kullanacağınız formül ESR kısaltmasıyla bilinir: önce Eşitlik alanları, sonra Sıralama alanları, en sonda Range yani aralık alanları. Şu sorguyu ele alalım:

    db.siparisler.find({
      musteriId: 4821,                              // eşitlik
      tutar: { $gte: 100 }                          // aralık
    }).sort({ olusturma: -1 })                      // sıralama
    

    ESR'ye göre doğru index { musteriId: 1, olusturma: -1, tutar: 1 } olur. Aralık alanını sıralamadan önce koyarsanız index sıralamayı karşılayamaz ve MongoDB bellekte ek bir SORT aşaması ekler. Farkı ölçmek için ikisini de oluşturup hint() ile karşılaştırabilirsiniz:

    // Doğru sıra (ESR)
    db.siparisler.createIndex({ musteriId: 1, olusturma: -1, tutar: 1 }, { name: "esr_dogru" })
    // Yanlış sıra (aralık, sıralamadan önce)
    db.siparisler.createIndex({ musteriId: 1, tutar: 1, olusturma: -1 }, { name: "esr_yanlis" })
    
    // İkisini de zorla çalıştırıp karşılaştır
    db.siparisler.find({ musteriId: 4821, tutar: { $gte: 100 } })
      .sort({ olusturma: -1 }).hint("esr_dogru").explain("executionStats").executionStats
    
    db.siparisler.find({ musteriId: 4821, tutar: { $gte: 100 } })
      .sort({ olusturma: -1 }).hint("esr_yanlis").explain("executionStats").executionStats
    

    Yanlış sıradaki planda SORT aşamasını ve belirgin biçimde yüksek totalKeysExamined değerini göreceksiniz. Ölçtükten sonra kaybedeni silin:

    db.siparisler.dropIndex("esr_yanlis")
    

    Sıralama yönü de önemlidir ama tahmin edilenden esnektir. MongoDB bir index'i ters yönde de tarayabilir, yani { olusturma: -1 } index'i sort({ olusturma: 1 }) sorgusuna da hizmet eder. Ancak bu esneklik yalnızca tüm sıralama alanlarının yönü topluca ters çevrildiğinde geçerlidir; { a: 1, b: -1 } sıralaması için { a: 1, b: 1 } index'i işe yaramaz.

    Kapsayan Sorgular ve Projeksiyon#

    En hızlı belge okuması, hiç belge okumamaktır. Sorgunuzun ihtiyaç duyduğu tüm alanlar index'in içinde varsa MongoDB belgelere hiç dokunmadan cevabı doğrudan index'ten üretir; buna kapsayan sorgu (covered query) denir ve explain çıktısında totalDocsExamined: 0 olarak görünür.

    // Index: musteriId + durum + tutar
    db.siparisler.createIndex({ musteriId: 1, durum: 1, tutar: 1 }, { name: "kapsayan" })
    
    // Projeksiyonda _id'yi KAPATMAK zorunludur, aksi hâlde belge okunur
    db.siparisler.find(
      { musteriId: 4821, durum: "onaylandi" },
      { _id: 0, durum: 1, tutar: 1 }
    ).explain("executionStats")
    // executionStats.totalDocsExamined: 0
    // winningPlan içinde PROJECTION_COVERED aşaması görünür
    

    _id: 0 satırını unutmak, kapsayan sorgu denemelerinin başarısız olmasının bir numaralı sebebidir: _id index'in parçası olmadığı için MongoDB onu almak üzere belgeye gitmek zorunda kalır ve kazanç kaybolur. Kapsayan sorgular özellikle sayaç, liste ve rapor uç noktalarında dramatik fark yaratır; disk erişimi tamamen ortadan kalktığı için gecikme neredeyse bellek hızına iner.

    Bunun tersi de doğrudur: gereksiz alan çekmek pahalıdır. Uygulamanız yalnızca üç alan kullanıyorsa 40 alanlı belgenin tamamını çekmeyin. Hem ağ trafiği hem de sunucu belleği bundan doğrudan etkilenir. Projeksiyonu her sorguya yazmayı alışkanlık hâline getirin.

    Yavaş Sorguları Otomatik Yakalamak#

    Elle explain çalıştırmak, hangi sorgunun yavaş olduğunu zaten bildiğiniz durumda işe yarar. Bilmediğiniz durumda profilleyiciyi devreye alın. MongoDB, belirlediğiniz eşiği aşan her işlemi system.profile koleksiyonuna yazar:

    // Seviye 1: yalnızca yavaş işlemleri kaydet, eşik 100 ms
    db.setProfilingLevel(1, { slowms: 100 })
    
    // Mevcut ayarı görüntüle
    db.getProfilingStatus()
    
    // En yavaş son 10 işlemi listele
    db.system.profile.find({ millis: { $gt: 100 } })
      .sort({ ts: -1 })
      .limit(10)
      .projection({ op: 1, ns: 1, millis: 1, planSummary: 1, "command.filter": 1 })
    

    planSummary alanı en değerli sütundur: COLLSCAN yazan her satır, index eksikliğinin doğrudan kanıtıdır. Profilleyiciyi seviye 2 (her işlem) ile üretimde çalıştırmayın; ciddi yazma yükü oluşturur ve system.profile koleksiyonunu şişirir. Seviye 1 ile makul bir eşik, üretimde günlerce açık kalabilir.

    Şu anda çalışan ve takılmış bir sorguyu görmek için ise currentOp kullanılır:

    // 5 saniyeden uzun süredir çalışan işlemler
    db.adminCommand({
      currentOp: true,
      active: true,
      secs_running: { $gte: 5 }
    })
    
    // Kilitlenmiş bir işlemi sonlandır (opid çıktıdan alınır)
    db.adminCommand({ killOp: 1, op: 12345678 })
    

    Bir index'in gerçekten kullanılıp kullanılmadığını ölçmek için $indexStats aşaması vardır ve gereksiz index avında paha biçilmezdir:

    db.siparisler.aggregate([{ $indexStats: {} }])
      .toArray()
      .map(i => ({ ad: i.name, kullanim: i.accesses.ops, since: i.accesses.since }))
    

    kullanim değeri günlerdir sıfır kalan bir index, koleksiyona yalnızca yük bindiriyordur. Aynı mantığı ilişkisel tarafta uygulamak isterseniz MySQL yavaş sorgu bulma yazısındaki yaklaşım birebir aynı felsefeye dayanır.

    Index'in Zarar Verdiği Durumlar ve Sık Hatalar#

    Index bedava değildir. Her index, koleksiyona yapılan her ekleme, güncelleme ve silme işleminde ayrıca güncellenmek zorundadır. Yani on index'li bir koleksiyonda tek bir belge eklemek, aslında on bir yazma işlemi demektir. Ayrıca her index bellekte yer kaplar ve çalışma kümesi (working set) bellekten taştığı anda tüm sorgular yavaşlar.

    En sık gördüğüm hataları sıralayayım:

    1. Her alana ayrı index açmak. Beş alanlı bir filtre için beş tek alan index'i açmak yerine, tek bir doğru sıralı bileşik index çoğu zaman daha iyidir. MongoDB genellikle sorgu başına tek bir index kullanır; index kesişimi (intersection) desteklense de planlayıcı buna nadiren başvurur.
    2. Önek kuralını yok saymak. { a: 1, b: 1 } varken ayrıca { a: 1 } açmak gereksizdir; ikincisi birincinin öneki olduğu için zaten kapsanır. Bu tür fazlalıkları getIndexes() çıktısını gözden geçirerek bulun.
    db.siparisler.getIndexes()
    db.siparisler.stats().indexSizes   // her index'in bayt cinsinden boyutu
    
    1. Düşük seçicilikli alana index açmak. İki değeri olan bir aktif alanına açılan index neredeyse hiç işe yaramaz, çünkü koleksiyonun yarısını işaret eder. Böyle alanları bileşik index'in içinde, seçici bir alanın yanında kullanın.
    2. $ne, $nin ve $not ile filtrelemek. Bu operatörler index'i verimli kullanamaz, çünkü "eşit olmayan" ifadesi index'in neredeyse tamamını taramak anlamına gelir. Mümkünse olumsuz koşulu olumluya çevirin: durum: { $ne: "iptal" } yerine durum: { $in: ["bekliyor", "onaylandi", "tamamlandi"] } yazın.
    3. Bağlanmamış düzenli ifade kullanmak. { ad: /ahmet/ } index kullanamaz, ama { ad: /^ahmet/ } kullanabilir; çünkü başa sabitlenmiş kalıp, index üzerinde bir aralığa karşılık gelir. Serbest metin arıyorsanız düzenli ifade yerine metin index'i ya da ayrı bir arama motoru düşünün.
    4. Üretimde index'i mesai saatinde oluşturmak. Büyük koleksiyonlarda index oluşturma disk ve işlemci yükü yaratır. Replica set kullanıyorsanız yükü dağıtmak mümkündür; yapı hakkında bilgi için MongoDB replica set kurulumu yazısına bakabilirsiniz.

    Aggregation Pipeline Optimizasyonu#

    Toplama (aggregation) sorgularında kural nettir: veriyi mümkün olduğunca erken azaltın. $match ve $limit aşamalarını hattın başına koyun ki sonraki aşamalar daha az belgeyle çalışsın. $match hattın ilk aşamasıysa index kullanabilir; araya $project ya da $addFields girdikten sonra gelen bir $match ise artık index'ten yararlanamaz.

    // KÖTÜ: önce hesapla, sonra filtrele
    db.siparisler.aggregate([
      { $addFields: { yil: { $year: "$olusturma" } } },
      { $match: { yil: 2026, musteriId: 4821 } },   // index kullanamaz
      { $group: { _id: "$durum", toplam: { $sum: "$tutar" } } }
    ])
    
    // İYİ: önce index'li filtre, sonra hesapla
    db.siparisler.aggregate([
      { $match: {
          musteriId: 4821,
          olusturma: { $gte: ISODate("2026-01-01"), $lt: ISODate("2027-01-01") }
      } },                                            // IXSCAN
      { $group: { _id: "$durum", toplam: { $sum: "$tutar" } } }
    ])
    

    İkinci sürümde tarihi hesaplanmış bir alanla değil, doğrudan aralık koşuluyla filtrelediğimize dikkat edin; bu, index'in kullanılabilmesinin şartıdır. Hattın plansal davranışını görmek için toplama sorgularında da explain çalışır:

    db.siparisler.explain("executionStats").aggregate([
      { $match: { musteriId: 4821 } },
      { $group: { _id: "$durum", toplam: { $sum: "$tutar" } } }
    ])
    

    Büyük toplama işlerinde bellek sınırına takılırsanız allowDiskUse: true seçeneği geçici dosyalara izin verir, ama bu bir çözüm değil bir emniyet supabıdır: diske taşan bir hat, doğru index'le yeniden tasarlanmalıdır. $lookup kullanıyorsanız da hedef koleksiyondaki eşleşme alanının mutlaka index'li olması gerekir; aksi hâlde her belge için bir koleksiyon taraması yaparsınız ve hat katlanarak yavaşlar.

    Sıkça Sorulan Sorular#

    MongoDB'de index'in kullanılıp kullanılmadığını nasıl anlarım#

    Sorgunun sonuna .explain("executionStats") ekleyin ve çıktıdaki winningPlan bölümüne bakın. IXSCAN aşaması varsa index kullanılıyor, COLLSCAN varsa koleksiyonun tamamı taranıyor demektir. Ayrıca totalDocsExamined değerini nReturned ile karşılaştırın; ikisi birbirine yakınsa sorgunuz verimli, aradaki fark büyükse index tasarımınız yetersizdir.

    Kaç tane index açmalıyım#

    Sabit bir sayı yok ama pratik bir sınır var: koleksiyon başına en fazla 64 index tanımlanabilir ve bu sınıra yaklaşmak zaten tasarım sorununun işaretidir. Doğru yaklaşım, uygulamanızın gerçekten çalıştırdığı sorgu desenlerini listeleyip her desen için bir bileşik index tasarlamaktır. $indexStats ile hiç kullanılmayanları düzenli olarak tespit edip silin.

    Bileşik index'te alan sırası neden önemli#

    MongoDB bileşik index'i yalnızca soldan başlayan öneklerle kullanabilir; { a, b, c } index'i a ve a+b sorgularına hizmet eder ama tek başına b sorgusuna edemez. Ayrıca sıra, sıralamanın index'ten karşılanıp karşılanamayacağını da belirler. Eşitlik, sıralama, aralık (ESR) sırasını izlerseniz hem filtre hem sıralama tek index'ten karşılanır ve bellekte ek sıralama aşaması oluşmaz.

    Index oluşturmak koleksiyonu kilitler mi#

    Modern MongoDB sürümlerinde index oluşturma, koleksiyonu okuma ve yazmaya kapatmadan yürütülür; işlem sırasında koleksiyon kullanılabilir kalır. Ancak disk ve işlemci yükü ciddi olabilir, büyük koleksiyonlarda saatler sürebilir. Bu yüzden yoğun saatlerde başlatmayın ve replica set kullanıyorsanız yükü izleyerek ilerleyin.

    Yavaş sorguları kalıcı olarak nasıl kaydederim#

    db.setProfilingLevel(1, { slowms: 100 }) komutuyla profilleyiciyi açın; bu ayar, 100 milisaniyeyi aşan işlemleri system.profile koleksiyonuna yazar. Kayıtları planSummary alanına göre süzerek COLLSCAN yapan sorguları bulabilirsiniz. Seviye 2 her işlemi kaydeder ve üretimde ciddi yük getirir, bu yüzden yalnızca kısa teşhis pencerelerinde kullanın.

    Metin araması için index yeterli mi yoksa arama motoru mu kullanmalıyım#

    Basit anahtar kelime eşleşmeleri ve düşük hacimli koleksiyonlar için MongoDB'nin metin index'i yeterlidir. Ancak dil analizi, eş anlamlı sözlük, yazım hatası toleransı, alan bazlı ağırlıklandırma ve karmaşık alaka sıralaması gerekiyorsa özel bir arama motoru çok daha iyi sonuç verir. Böyle bir ihtiyaç oluştuğunda veriyi MongoDB'de tutup arama katmanını ayrı bir sisteme devretmek yaygın ve sağlıklı bir mimaridir.

    Kapanış#

    MongoDB'de performans, gizemli bir ayar dosyasında değil, sorgu planında saklıdır. Aklınızda tutmanız gereken dört alışkanlık şu: her yeni sorgu deseni için explain("executionStats") çalıştırıp totalDocsExamined ile nReturned oranına bakın, bileşik index'lerde ESR sırasını uygulayın, sık çalışan liste ve sayaç sorgularını kapsayan hâle getirmek için projeksiyon yazın ve $indexStats ile kullanılmayan index'leri düzenli olarak temizleyin. Index eklemek kadar silmek de optimizasyondur.

    Bu ölçümleri yapabilmek için önce sabit ve öngörülebilir bir donanıma ihtiyacınız var; paylaşımlı ortamda yapılan ölçümler yanıltıcı olur. Kendi MongoDB örneğinizi tam kaynak garantisiyle çalıştırmak isterseniz VDS ve bulut sunucu paketlerimiz uygun bir zemin sunar; veri hacmi büyüdükçe disk ve bellek planlamasını, index bakımını ve izlemeyi bize bırakmak isterseniz sunucu yönetimi hizmetimiz devreye girer.

    MongoDBIndexPerformans

    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.