MongoDB Index Stratejileri ve Performans Analizi

Veritabanı yönetiminde en çok zaman kaybettiren konuların başında index stratejileri gelir. “Neden bu sorgu bu kadar yavaş?” sorusunu soran her sysadmin eninde sonunda MongoDB’nin index mekanizmasıyla yüzleşmek zorunda kalır. Bu yazıda gerçek dünya senaryoları üzerinden MongoDB index stratejilerini, performans analizini ve dikkat edilmesi gereken tuzakları ele alacağız.

MongoDB Index Temelleri ve Neden Bu Kadar Önemli

MongoDB’de index olmadan yapılan her sorgu, koleksiyonun tamamını tarar. Buna collection scan denir ve milyonlarca doküman içeren koleksiyonlarda bu durum ciddi performans sorunlarına yol açar. Bir e-ticaret sitesinde çalışırken yaşadığım şu senaryoyu düşünün: 50 milyonluk sipariş koleksiyonunda status alanına göre filtreleme yapılan bir sorgu, her gece raporlama sırasında sunucuyu neredeyse çökertiyordu. Sorunun cevabı basitti: index yoktu.

MongoDB’nin sunduğu index türleri oldukça geniş bir yelpazede. Temel olarak şunları bilmek gerekiyor:

  • Single Field Index: Tek alan üzerinde oluşturulan en basit index türü
  • Compound Index: Birden fazla alan üzerinde oluşturulan bileşik index
  • Multikey Index: Array alanları için otomatik olarak oluşturulan index türü
  • Text Index: Full-text arama için kullanılan özel index
  • Geospatial Index: Coğrafi veri sorguları için kullanılan index
  • Hashed Index: Sharding senaryolarında kullanılan hash tabanlı index
  • Wildcard Index: Dinamik alan yapılarında kullanılan esnek index türü

Mevcut Index Durumunu Analiz Etmek

Bir sorunla karşılaştığınızda ilk yapmanız gereken mevcut index yapısını görmek. Bunun için şu komutları kullanabilirsiniz:

# MongoDB shell üzerinden index listesi
mongosh --eval "db.orders.getIndexes()" --quiet

# Koleksiyona ait tüm indexleri daha okunabilir formatta görme
mongosh mydb --eval "
db.orders.getIndexes().forEach(function(idx) {
    printjson(idx);
})"

Index boyutlarını ve kullanım istatistiklerini görmek için $indexStats aggregation pipeline’ını kullanabilirsiniz:

mongosh mydb --eval "
db.orders.aggregate([
    { $indexStats: {} }
]).forEach(function(stat) {
    print('Index: ' + stat.name + 
          ' | Kullanim: ' + stat.accesses.ops + 
          ' | Son Erisim: ' + stat.accesses.since);
})"

Bu komutun çıktısı size hangi indexlerin gerçekten kullanıldığını, hangilerinin sadece disk ve bellek harcadığını gösterir. Hiç kullanılmayan indexler hem write performansını düşürür hem de RAM tüketir. Bunu fark ettikten sonra bir projede 12 indexten 5’ini silerek write throughput’u yüzde otuz artırdığımı hatırlıyorum.

explain() ile Sorgu Analizinin Derinliklerine İnmek

MongoDB’de performans sorununu tespit etmenin en doğru yolu explain() metodunu kullanmaktır. Bu metot size sorgunun nasıl çalıştığına dair detaylı bilgi verir.

mongosh mydb --eval "
var result = db.orders.find(
    { status: 'pending', createdAt: { $gte: new Date('2024-01-01') } }
).explain('executionStats');

print('Sorgu Modu: ' + result.queryPlanner.winningPlan.stage);
print('Taranan Dokumanlar: ' + result.executionStats.totalDocsExamined);
print('Dönen Dokumanlar: ' + result.executionStats.totalDocsReturned);
print('Sorgu Suresi (ms): ' + result.executionStats.executionTimeMillis);
print('Index Kullanildi mi: ' + (result.queryPlanner.winningPlan.stage !== 'COLLSCAN'));
"

Burada dikkat etmeniz gereken birkaç kritik değer var:

  • COLLSCAN: Collection scan gerçekleşiyor, index yok veya kullanılmıyor
  • IXSCAN: Index scan gerçekleşiyor, iyi haber
  • totalDocsExamined / totalDocsReturned oranı: Bu oran 1’e yakınsa index verimli çalışıyor. Oran çok yüksekse index seçici değil demektir
  • executionTimeMillis: Milisaniye cinsinden çalışma süresi

Compound Index Stratejileri: ESR Kuralı

Compound index oluştururken en sık yapılan hata alan sırasını yanlış belirlemektir. MongoDB’nin önerdiği ESR (Equality, Sort, Range) kuralı bu konuda rehber niteliğindedir.

ESR kuralına göre index alanlarını şu sırayla belirlemelisiniz:

  • Equality (Eşitlik): Tam eşleşme yapılan alanlar önce gelir
  • Sort (Sıralama): Order by içindeki alanlar ortada yer alır
  • Range (Aralık): Büyük/küçük gibi aralık sorguları en sona gelir

Gerçek bir örnek üzerinden gösterelim. Bir lojistik uygulamasında şu sorgu sık kullanılıyordu:

# Verimssiz sorgu - index yok
mongosh logistics --eval "
db.shipments.find({
    companyId: 'COMP001',
    status: 'in_transit',
    estimatedDelivery: { $gte: new Date(), $lte: new Date(Date.now() + 86400000) }
}).sort({ priority: -1 }).explain('executionStats')
"

Bu sorgu için ESR kuralına göre doğru index şöyle oluşturulur:

# ESR kuralina gore dogru compound index
mongosh logistics --eval "
db.shipments.createIndex(
    { 
        companyId: 1,    // Equality
        status: 1,       // Equality  
        priority: -1,    // Sort
        estimatedDelivery: 1  // Range
    },
    { 
        name: 'idx_company_status_priority_delivery',
        background: true
    }
)"

background: true parametresi önemli. Büyük koleksiyonlarda index oluşturma işlemi sırasında koleksiyonun kilitlenmesini engeller. MongoDB 4.4 ve sonrasında bu davranış zaten varsayılan olsa da eski versiyonlarda kritiktir.

Partial Index ile Akıllı Filtreleme

Her kayıt için index oluşturmak zorunda değilsiniz. Partial index yalnızca belirtilen koşulu karşılayan dökümanları indexler. Bu özellikle “aktif” veya “işlenmemiş” gibi sadece bir kısmının sorgulandığı alanlarda mükemmel çalışır.

# Sadece aktif ve onay bekleyen siparisleri indexle
mongosh ecommerce --eval "
db.orders.createIndex(
    { createdAt: 1, customerId: 1 },
    {
        partialFilterExpression: {
            status: { $in: ['pending', 'processing'] }
        },
        name: 'idx_active_orders_partial'
    }
)"

Bu yaklaşımla bir müşteri projesinde index boyutunu yüzde altmış sekiz oranında küçülttük. Siparişlerin yüzde doksanı “completed” durumundaydı ve bu siparişlerin sorgulanma oranı son derece düşüktü.

Slow Query Log’u Aktif Hale Getirmek

Hangi sorguların yavaş olduğunu tespit etmek için MongoDB’nin profiler özelliğini kullanabilirsiniz. Production ortamında bunu dikkatli kullanmanız gerekir, çünkü yüksek profiling seviyesi performansı etkiler.

# Profiling seviyesini ayarla (0=off, 1=slow, 2=all)
# 1000ms'den uzun suren sorgulari logla
mongosh mydb --eval "
db.setProfilingLevel(1, { slowms: 1000 });
print('Profiling durumu: ');
printjson(db.getProfilingStatus());
"

Profiler verilerini analiz etmek için:

# Son 10 yavas sorguyu goster, en yavastan en hizliya sirala
mongosh mydb --eval "
db.system.profile.find(
    { millis: { $gt: 1000 } }
).sort({ millis: -1 }).limit(10).forEach(function(doc) {
    print('---');
    print('Koleksiyon: ' + doc.ns);
    print('Süre: ' + doc.millis + 'ms');
    print('Taranan Dok: ' + doc.docsExamined);
    print('Dönen Dok: ' + doc.nreturned);
    if(doc.query) print('Query: ' + JSON.stringify(doc.query));
    if(doc.command) print('Command: ' + JSON.stringify(doc.command).substring(0, 200));
})"

Index Intersection ve Covering Index Kavramı

MongoDB bazı durumlarda birden fazla index’i birleştirerek kullanabilir. Buna index intersection denir. Ancak bu mekanizmaya güvenmek yerine doğru compound index oluşturmak her zaman daha iyi performans sağlar.

Öte yandan covering index çok daha faydalı bir kavram. Sorgunun ihtiyaç duyduğu tüm alanlar index içindeyse, MongoDB dökümanların kendisine hiç bakmadan sadece index üzerinden sonucu döner. Bu özellikle büyük dökümanlar için inanılmaz performans artışı sağlar.

# Covering index ornegi
# Sorgu: belirli bir userId'nin son 30 gundeki siparis sayisi ve toplam tutari
mongosh ecommerce --eval "
// Once covering index olustur
db.orders.createIndex(
    { userId: 1, createdAt: -1, totalAmount: 1, status: 1 },
    { name: 'idx_covering_user_orders' }
);

// Projection ile sadece indexte olan alanlari sec
// MongoDB dokumana hic bakmaycak, sadece index kullanacak
var result = db.orders.find(
    { 
        userId: 'user123',
        createdAt: { $gte: new Date(Date.now() - 30*24*60*60*1000) }
    },
    { userId: 1, totalAmount: 1, status: 1, _id: 0 }
).explain('executionStats');

print('Index Only Scan: ' + (result.executionStats.totalDocsExamined === 0));
print('Süre: ' + result.executionStats.executionTimeMillis + 'ms');
"

totalDocsExamined: 0 görüyorsanız mükemmel, sorgu tamamen index üzerinden çözüldü demektir.

Gerçek Dünya Senaryosu: Sosyal Medya Uygulaması Feed Optimizasyonu

Bir sosyal medya benzeri uygulamada “takip ettiğin kişilerin son 24 saatteki gönderileri” sorgusunu optimize etmemiz gerekiyordu. Başlangıç durumu şöyleydi:

  • 80 milyon gönderi
  • Her sorgu ortalama 4-8 saniye sürüyordu
  • Sunucu CPU’su sürekli yüzde doksanın üzerindeydi
# Sorunlu sorgu (sadece createdAt indexi vardi)
mongosh social --eval "
db.posts.find({
    authorId: { $in: followingList },  // 500 kisi takip ediliyor
    createdAt: { $gte: new Date(Date.now() - 86400000) },
    isDeleted: false,
    visibility: 'public'
}).sort({ createdAt: -1 }).limit(50).explain('executionStats')
"

Analiz sonucunda sorunları tespit ettik:

  • isDeleted: false olan kayıtlar toplamın yüzde doksan sekiziydi, partial index fırsatı
  • visibility: 'public' olan kayıtlar toplamın yüzde sekseniydi
  • $in ile 500 kişilik liste sorgusu ayrı bir sorundu

Çözüm olarak üç adımlı bir yaklaşım uyguladık:

# Adim 1: Partial index ile silinmemis ve public gonderileri indexle
mongosh social --eval "
db.posts.createIndex(
    { authorId: 1, createdAt: -1 },
    {
        partialFilterExpression: {
            isDeleted: false,
            visibility: 'public'
        },
        name: 'idx_active_public_posts'
    }
)"

# Adim 2: Gereksiz indexleri temizle
mongosh social --eval "
// Hangi indexler hic kullanilmamis?
db.posts.aggregate([
    { $indexStats: {} },
    { $match: { 'accesses.ops': { $lt: 100 } } },
    { $project: { name: 1, 'accesses.ops': 1 } }
]).forEach(printjson)
"

Sonuç olarak ortalama sorgu süresi 4-8 saniyeden 80-120 milisaniyeye düştü. CPU kullanımı yüzde otuzun altına geriledi.

Index Bakımı ve Yeniden Oluşturma

Zamanla fragmente olan indexler performansı düşürebilir. reIndex() komutu bu sorunu çözer ancak production ortamında dikkatli kullanılmalıdır.

# Index istatistiklerini kontrol et
mongosh mydb --eval "
var stats = db.orders.stats({ indexDetails: true });
print('Toplam Index Boyutu: ' + Math.round(stats.totalIndexSize / 1024 / 1024) + ' MB');
Object.keys(stats.indexDetails).forEach(function(indexName) {
    var detail = stats.indexDetails[indexName];
    print('Index: ' + indexName);
    // WiredTiger cache hit ratio
    if(detail.cache) {
        print('  Cache hit ratio: ' + 
            Math.round(detail.cache['bytes currently in the cache'] / 
            detail.cache['bytes read into cache'] * 100) + '%');
    }
});
"

Replica set ortamında index yeniden oluşturmayı güvenli yapmak için önce secondary’lerde, sonra primary’de yapmanız gerekir:

# Secondary node'da index yeniden olusturma
# Once maintenance mode'a al
mongosh --port 27018 --eval "
rs.secondaryOk();
db.orders.reIndex();
print('Index yeniden olusturuldu');
"

Wildcard Index Kullanımı

Şema esnekliği gerektiren uygulamalarda, özellikle e-ticaret ürün özellikleri gibi dinamik alanlarda wildcard index çok işe yarar:

# Urun ozellikleri icin wildcard index
mongosh catalog --eval "
// Her urunun farkli ozellikleri olabilir: renk, beden, malzeme vb.
db.products.createIndex(
    { 'attributes.$**': 1 },
    { name: 'idx_product_attributes_wildcard' }
);

// Bu index asagidaki gibi sorgulari optimize eder:
db.products.find({ 'attributes.color': 'red' }).explain();
db.products.find({ 'attributes.size': 'XL' }).explain();
db.products.find({ 'attributes.material': 'cotton' }).explain();
"

Wildcard index’in dezavantajı, her field için ayrı bir index girişi oluşturduğundan bellek tüketiminin yüksek olabileceğidir. Bu yüzden compound wildcard yerine belirli bir alt ağaç için kullanmak daha akıllıca olur.

Index Kullanımını Monitoring Etmek

Production ortamında indexlerin düzgün kullanıldığını sürekli takip etmek için basit bir monitoring script’i oluşturabilirsiniz:

#!/bin/bash
# /usr/local/bin/mongo_index_monitor.sh

MONGOSH_CMD="mongosh --quiet"
DB_NAME="production"
ALERT_THRESHOLD=10000  # 10 saniyeden uzun suren sorgular icin

echo "=== MongoDB Index Monitoring Raporu ===" 
echo "Tarih: $(date)"
echo ""

# Hic kullanilmayan indexleri bul
echo "--- Kullanilmayan Indexler ---"
$MONGOSH_CMD $DB_NAME --eval "
db.getCollectionNames().forEach(function(collName) {
    db[collName].aggregate([
        { $indexStats: {} },
        { $match: { 'accesses.ops': 0, name: { $ne: '_id_' } } }
    ]).forEach(function(idx) {
        print('UYARI: ' + collName + ' -> ' + idx.name + ' hic kullanilmamis');
    });
});"

# Son 1 saatteki yavas sorgulari say  
echo ""
echo "--- Son 1 Saatteki Yavas Sorgular ---"
$MONGOSH_CMD $DB_NAME --eval "
var oneHourAgo = new Date(Date.now() - 3600000);
var count = db.system.profile.countDocuments({
    ts: { $gte: oneHourAgo },
    millis: { $gte: $ALERT_THRESHOLD }
});
print('10 saniyeden uzun suren sorgu sayisi: ' + count);"

echo ""
echo "Monitoring tamamlandi"

Bu scripti cron ile her saat çalıştırıp çıktısını log’a yönlendirebilirsiniz:

# Crontab'a ekle
echo "0 * * * * /usr/local/bin/mongo_index_monitor.sh >> /var/log/mongo_index_monitor.log 2>&1" | crontab -

Sık Yapılan Hatalar ve Kaçınma Yolları

Yıllar içinde gördüğüm en yaygın index hatalarından bahsedelim:

  • Aşırı index oluşturma: Her alana index koymak write performansını dramatik şekilde düşürür. Her insert ve update işlemi tüm indexleri güncellemelidir. Genel kural olarak bir koleksiyonda 5-6 indexi geçmemeye çalışın
  • Index seçiciliğini görmezden gelmek: Boolean bir alan için single index oluşturmak neredeyse işe yaramaz. true/false değerinin koleksiyonun yüzde ellisinde olduğunu düşünün, index size çok az fayda sağlar
  • Sıralama yönünü dikkate almamak: createdAt: 1 ile createdAt: -1 farklı indexlerdir. Uygulamanız hep descending sıralama yapıyorsa ascending index oluştururak ters sıralama yaptırmak biraz performans kaybettirir, yönü doğru belirleyin
  • $or sorgularında compound index beklentisi: $or sorguları compound indexten faydalanamaz. Her $or koşulu için ayrı index gerekir
  • Regex sorgularında index kullanımı: ^ ile başlamayan regex sorguları index kullanamaz. db.users.find({ email: /gmail/ }) full scan yapar, db.users.find({ email: /^user/ }) index kullanabilir

Sonuç

MongoDB index stratejileri, veritabanı performansının kalbinde yer alır. Doğru index seçimi milisaniyeler ile dakikalar arasındaki farkı belirleyebilir. Bu yazıda ele aldığımız konuları özetlersek:

  • explain('executionStats') kullanımını alışkanlık haline getirin, kör kalmayın
  • ESR kuralını compound index tasarımında rehber olarak kullanın
  • Partial index ve covering index’i unutmayın, büyük koleksiyonlarda inanılmaz kazanımlar sağlar
  • $indexStats ile düzenli olarak kullanılmayan indexleri temizleyin
  • Slow query log’u aktif tutun ve düzenli analiz edin
  • Wildcard index’i dinamik şema yapıları için değerlendirin ama bellek kullanımını göz önünde bulundurun

Her uygulama kendi erişim desenine sahiptir. Bu yüzden index stratejisi de uygulamaya özgü olmalıdır. Generic bir çözüm yerine, kendi uygulamanızın sorgu pattern’larını analiz edip ona göre index oluşturmak uzun vadede en doğru yaklaşımdır. Production’a geçmeden önce explain() ile tüm kritik sorgularınızı test edin, beklenmedik collection scan’larla karşılaşmayın.

Yorum yapın