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:
| Alan | Anlamı | İdeal durum |
|---|---|---|
nReturned | Sorgunun döndürdüğü belge sayısı | İhtiyacınız olan sayı |
totalKeysExamined | Taranan index anahtarı sayısı | nReturned'a yakın |
totalDocsExamined | Okunan belge sayısı | nReturned'a yakın, ideali 0 |
executionTimeMillis | Toplam süre | Mümkün olduğunca düşük |
winningPlan.stage | Kullanılan erişim yöntemi | IXSCAN, 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ür | Ne için | Örnek |
|---|---|---|
| Tek alan | Basit eşitlik ve aralık sorguları | { email: 1 } |
| Bileşik | Birden fazla alanla filtre ve sıralama | { musteriId: 1, olusturma: -1 } |
| Çok anahtarlı | Dizi alanları (otomatik oluşur) | { etiketler: 1 } |
| Benzersiz | Tekrarı engelleme | { email: 1 }, { unique: true } |
| Kısmi | Yalnızca belgelerin bir alt kümesi | partialFilterExpression |
| Seyrek | Alanı olmayan belgeleri dışarıda bırakma | { sparse: true } |
| TTL | Belgeyi süre sonunda otomatik silme | expireAfterSeconds |
| Metin | Serbest metin arama | { aciklama: "text" } |
| Hash | Parç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:
- 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.
- Ö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
- Düşük seçicilikli alana index açmak. İki değeri olan bir
aktifalanı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. $ne,$ninve$notile 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" }yerinedurum: { $in: ["bekliyor", "onaylandi", "tamamlandi"] }yazın.- 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. - Ü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.