MongoDB'de index oluşturmak için db.koleksiyon.createIndex({ alan: 1 }) komutunu kullanırsınız; 1 artan, -1 azalan sıralı bir index anlamına gelir. Var olan indexleri getIndexes() ile listeler, ihtiyacınız kalmayanları dropIndex() ile silersiniz. Index olmayan bir sorguda MongoDB koleksiyondaki her belgeyi tek tek okumak zorunda kalır; uygun bir index varsa yalnızca ilgili kayıtlara bakar.

Bu yazıda tek alanlı (single field) indexi, gömülü alan ve gömülü belge üzerindeki indexleri, index adlandırma kurallarını, dolu bir koleksiyonda index oluşturulurken arka planda neler olduğunu ve indexleri güvenli biçimde nasıl kaldıracağınızı MongoDB 9.0 belgelerine dayanarak anlatıyoruz. Birden fazla alanlı indexler için compound index ve ESR kuralı, unique, partial ve hidden gibi özellikler için index türleri yazısına geçebilirsiniz.

Index nedir ve neden gerekir?

MongoDB belgelerine göre index, koleksiyondaki verinin küçük bir bölümünü kolay taranabilir bir biçimde saklayan özel bir veri yapısıdır ve MongoDB indexleri B-tree yapısını kullanır. Index, bir alanın ya da alan grubunun değerlerini sıralı olarak tutar. Bu sıralama hem eşitlik aramalarını ({ sehir: "İzmir" }) hem de aralık sorgularını ({ fiyat: { $gt: 100 } }) hızlandırır; ayrıca MongoDB sonuçları index sırasından okuyarak ayrı bir sıralama adımına gerek duymadan döndürebilir.

Index yoksa sorgu planı COLLSCAN yani koleksiyon taraması olur: sunucu bütün belgeleri okuyup filtreye uyanları ayıklar. Küçük bir koleksiyonda bu fark edilmez, ancak belge sayısı büyüdükçe aynı sorgu hem daha uzun sürer hem de daha fazla bellek ve disk okuması harcar. Bir sorgunun index kullanıp kullanmadığını explain() ile sorgu planı okuma yazısında anlattığımız yöntemle görebilirsiniz.

Indexin bir bedeli de var: her ekleme ve güncellemede MongoDB yalnızca belgeyi değil, o belgeyi içeren bütün indexleri de günceller. Belgeler, yazma oranı okuma oranına göre yüksek koleksiyonlarda indexlerin pahalı olduğunu özellikle vurgular. Bu yüzden "her alana index" yaklaşımı yerine, uygulamanızın sık çalıştırdığı sorguları listeleyip yalnızca onları destekleyen indexleri oluşturmak gerekir.

Varsayılan _id indexi

Her koleksiyon oluşturulurken MongoDB _id alanı üzerinde otomatik olarak unique bir index kurar. Bu index aynı _id değerine sahip iki belgenin eklenmesini engeller ve silinemez; _id indexinden kurtulmanın tek yolu koleksiyonun tamamını silmektir. Sharded cluster kullanıyorsanız ve shard key _id değilse, belgeler _id değerlerinin benzersizliğini uygulamanın sağlaması gerektiğini, bunun için de otomatik üretilen ObjectId kullanılabileceğini söyler. Ayrıntılar için sharding ve shard key yazısına bakabilirsiniz.

createIndex sözdizimi

Metodun imzası db.collection.createIndex(keys, options, commitQuorum) biçimindedir. keys belgesinde alan adı anahtar, değer ise index türüdür: artan için 1, azalan için -1. Aynı yere "text", "2dsphere" veya "hashed" yazarak özel index türleri de kurabilirsiniz; bunları text index ve geospatial sorgular yazılarında ele alıyoruz. Belgeye göre * geçerli bir index adı değildir.

Aşağıdaki örnekte bir e-ticaret veritabanındaki urunler koleksiyonuna kategori alanı için artan bir index ekliyoruz. Belgelerdeki örneklere göre mongosh, oluşturulan indexin adını döndürür:

javascript
use magaza

db.urunler.createIndex( { kategori: 1 } )
// çıktı: kategori_1

// Bu index şu tür sorguları destekler:
db.urunler.find( { kategori: "kitap" } )
db.urunler.find( { kategori: { $in: [ "kitap", "dergi" ] } } )

Tek alanlı bir indexte yön seçimi sıralama açısından önemsizdir. Belgelere göre tek alan üzerindeki artan veya azalan bir index, o alana göre her iki yönde sıralamayı da destekler; MongoDB indexi gerektiğinde ters yönde dolaşır. Yani { kategori: 1 } indexi hem sort({ kategori: 1 }) hem de sort({ kategori: -1 }) için kullanılabilir. Yön, birden fazla alanı farklı yönlerde sıraladığınız compound indexlerde anlam kazanır.

Seçenekler belgesi

Seçenekler belgesi: name, unique, partialFilterExpression, sparse

options parametresi indexin nasıl kurulacağını belirler. En sık kullanılanlar name, unique, partialFilterExpression, sparse, expireAfterSeconds, hidden ve collation seçenekleridir. Birden çok seçeneği tek bir belgede verebilirsiniz; belgeler, seçenekleri birden fazla ayrı belgeye bölerseniz (örneğin { unique: true }, { sparse: true }) işlemin başarısız olacağını özellikle belirtir.

SeçenekNe işe yararAyrıntı
nameIndexe özel ad verirVerilmezse alan adı ve yönden üretilir
uniqueYinelenen değeri reddederVarsayılan false; hashed indexte kullanılamaz
partialFilterExpressionYalnızca filtreye uyan belgeleri indexlerIndex türleri yazısında
sparseAlanı olmayan belgeleri atlarPartial index tercih edilir
expireAfterSecondsBelgeyi süresi dolunca siler (TTL)0 ile 2147483647 arası
hiddenIndexi sorgu planlayıcıdan gizlerVarsayılan false
collationDile özgü metin karşılaştırmasıBüyük-küçük harf duyarsız index için

Bu seçeneklerin davranışını ayrı yazılarda inceliyoruz: benzersizlik, partial, sparse, hidden ve collation için unique, partial ve sparse index, otomatik silme için TTL index yazısına bakın.

Gömülü alan ve gömülü belge üzerinde index

Gömülü alan ve gömülü belge üzerinde index

Belgeleriniz iç içe yapılar içeriyorsa iki farklı yaklaşım vardır ve sonuçları birbirinden çok farklıdır. Birincisi nokta gösterimiyle (dot notation) tek bir iç alanı indexlemektir. Örneğin adres.il alanına index eklediğinizde { "adres.il": "Ankara" } gibi sorgular bu indexi kullanır.

javascript
db.musteriler.insertMany( [
   { ad: "Elif", adres: { il: "Ankara", ilce: "Çankaya" } },
   { ad: "Mert", adres: { il: "İzmir", ilce: "Bornova" } }
] )

// İç alan üzerinde index (nokta gösterimi)
db.musteriler.createIndex( { "adres.il": 1 } )

// Bu sorgular adres.il indexini kullanabilir
db.musteriler.find( { "adres.il": "Ankara" } )
db.musteriler.find( { "adres.il": "İzmir", "adres.ilce": "Bornova" } )

İkinci yaklaşım gömülü belgenin tamamını indexlemektir: createIndex({ adres: 1 }). Belgeler bunun bir tuzağı olduğunu söyler: böyle bir index yalnızca gömülü belgenin tamamını belirten sorgularda kullanılır. { "adres.il": "Ankara" } gibi bir iç alan sorgusu bu indexten yararlanamaz. Üstelik tam belge sorgusunda alanların sırası da önemlidir; { adres: { ilce: "Çankaya", il: "Ankara" } } sorgusu, belgedeki sıra farklı olduğu için hiç sonuç döndürmez.

javascript
db.musteriler.createIndex( { adres: 1 } )

// Gömülü belgenin tamamı, aynı alan sırasıyla: index kullanılabilir
db.musteriler.find( { adres: { il: "Ankara", ilce: "Çankaya" } } )

// İç alan sorgusu: { adres: 1 } indexini KULLANMAZ
db.musteriler.find( { "adres.ilce": "Çankaya" } )

Şemanız değiştikçe gömülü belgeye alan eklenir veya çıkarılır ve tam belge eşleşmesi beklenmedik biçimde bozulabilir. Bu nedenle belgeler, gömülü belgenin tamamını indexlemeden önce sorguladığınız iç alanları ayrı ayrı indexlemeyi ya da bütün alt alanları kapsayan bir wildcard index kullanmayı düşünmenizi önerir. Gömülü belge sorgularının kendisini find sorguları yazısında anlattık.

Dizi alanları: multikey index

Değeri dizi olan bir alana index eklediğinizde ayrıca bir şey belirtmeniz gerekmez: MongoDB indexi kendiliğinden multikey olarak kurar. Dizideki her farklı değer için ayrı bir index girdisi oluşur ve hepsi aynı belgeyi gösterir; dizide aynı değer birden çok kez geçiyorsa index bu değer için tek girdi tutar. Örneğin etiketler: [ "kampanya", "yeni" ] içeren belgeler için createIndex({ etiketler: 1 }) yeterlidir ve { etiketler: "yeni" } sorgusu bu indexi kullanır.

Multikey indexlerin birkaç kısıtı var: hashed indexler multikey olamaz, multikey bir index shard key indexi olarak seçilemez ve bir compound indexte belge başına en fazla bir alan dizi olabilir. MongoDB 9.0 ile gelen bir değişikliğe göre, indexli bir alan bir sorgu çalışırken diziye dönüşürse (yani bir ekleme veya güncelleme o alana dizi yazarsa) sorgu QueryKilledError ile başarısız olabilir; bu durumda yazma işlemi bittikten sonra sorguyu yeniden çalıştırmanız önerilir. Sürümdeki diğer değişiklikler için MongoDB 9.0 yenilikleri yazısına bakın. Dizi sorgularının mantığını dizi sorgulama ve $elemMatch yazısında bulabilirsiniz.

Index adları

Ad vermezseniz MongoDB, indexlenen alanların adlarını ve her alanın yönünü alt çizgiyle birleştirerek bir ad üretir. { kategori: 1, fiyat: -1 } için varsayılan ad kategori_1_fiyat_-1 olur. Index türleri de ada yansır: belgelerdeki örneklere göre { alanA: 1, alanB: "hashed", alanC: -1 } indexinin adı alanA_1_alanB_hashed_alanC_-1 biçiminde oluşur.

javascript
db.urunler.createIndex(
   { kategori: 1, fiyat: -1 },
   { name: "kategori_fiyat_liste" }
)
// çıktı: kategori_fiyat_liste

Anlamlı adlar, özellikle explain çıktısında hangi indexin seçildiğini okurken işinizi kolaylaştırır. İki kurala dikkat edin: index adları koleksiyon içinde benzersiz olmalıdır ve var olan bir adla yeni index oluşturmaya çalışmak hata döndürür; ayrıca bir indexin adı sonradan değiştirilemez. Adı değiştirmek istiyorsanız indexi silip yeni adla yeniden oluşturmanız gerekir.

Indexleri listeleme: getIndexes

Bir koleksiyondaki indexleri, o anda kurulmakta olanlar dahil, getIndexes() ile görürsünüz. Her girdide index sürümü (v), anahtar deseni (key) ve ad (name) bulunur; unique veya hidden gibi seçenekler de varsa ayrıca listelenir.

javascript
db.urunler.getIndexes()
// çıktı:
// [
//    { v: 2, key: { _id: 1 }, name: '_id_' },
//    { v: 2, key: { kategori: 1 }, name: 'kategori_1' }
// ]

Bir veritabanındaki bütün koleksiyonların indexlerini tek seferde görmek için belgelerde verilen yöntem, koleksiyon adlarını dolaşıp her biri için getIndexes() çağırmaktır:

javascript
db.getCollectionNames().forEach(function (koleksiyon) {
   const indexler = db[koleksiyon].getIndexes();
   print("Indexler: " + koleksiyon);
   printjson(indexler);
});

Dolu koleksiyonda index oluşturma

Dolu koleksiyonda index oluşturma: Eşzamanlı sınır, Bellek, Disk, Yoğun yazma

Boş bir koleksiyonda index anında oluşur; asıl dikkat isteyen, milyonlarca belge içeren canlı bir koleksiyona index eklemektir. Belgelere göre güncel index oluşturma süreci, koleksiyon üzerinde yalnızca başta ve sonda kısa süreli özel (exclusive) bir kilit tutar; aradaki uzun tarama aşamasında araya giren okuma ve yazma işlemlerine yol verir. Eski sürümlerdeki "foreground" ve "background" ayrımı artık yoktur ve createIndex()'e verilen background seçeneği yok sayılır.

Süreç kabaca şöyle işler: sunucu önce koleksiyonu kilitleyip index meta verisini, index sürerken yapılan yazmaları biriktiren geçici bir "side writes" tablosunu ve kural ihlali yaratabilecek anahtarlar için bir "constraint violation" tablosunu oluşturur. Ardından kilidi gevşetir, her belge için anahtar üretip harici bir sıralayıcıya atar ve sıralı anahtarları indexe yazar. Biriken yazmalar sırayla işlenir, sonda kısa bir kilitle kalan kayıtlar uygulanır, kural ihlalleri kontrol edilir ve index "kullanıma hazır" olarak işaretlenir.

Bunun pratik bir sonucu var: unique gibi kısıt içeren indexlerde ihlaller index oluşturmanın sonunda kontrol edilir. Koleksiyonda yinelenen değerler varsa oluşturma başlayabilir, ama bitişte hâlâ ihlal varsa sunucu işlemi durdurup hata verir; index sürerken uygulamanın yinelenen değer yazması da mümkündür. Belgeler bu riski azaltmak için önce verinin kısıtı ihlal etmediğini doğrulamayı ve ihlalsiz yazma garanti edemeyen uygulamaların yazmalarını durdurmayı önerir. Bu hatanın kendisini E11000 duplicate key hatası yazısında inceliyoruz.

  • Eşzamanlı sınır: Sunucu varsayılan olarak aynı anda en fazla üç index oluşturma işlemine izin verir; bu sayı maxNumActiveUserIndexBuilds parametresiyle değişir, sınıra ulaşılınca yeni işlemler sıraya girer.
  • Bellek: Kendi sunucunuzda maxIndexBuildMemoryUsageMegabytes varsayılan olarak her createIndexes komutu için 200 megabayttır ve bu bellek komuttaki indexler arasında eşit bölünür. Sınır aşılınca MongoDB --dbpath altındaki _tmp dizinine geçici dosyalar yazar.
  • Disk: MongoDB 7.1 ve sonrasında indexBuildMinAvailableDiskSpaceMB parametresiyle belirlenen boş alanın altına düşülürse index oluşturma durdurulabilir.
  • Yoğun yazma: Hedef koleksiyon ağır yazma altındayken index kurmak hem yazma performansını düşürür hem de süreyi uzatır; belgeler yazmaların azaldığı bir bakım penceresi önerir.

İlerlemeyi izlemek

Süren bir index oluşturma işlemini admin veritabanında $currentOp aşamasıyla görebilirsiniz. Belgeler, commit quorum beklemesi gibi bazı aşamalarda işlemin boşta (idle) bağlantı olarak işaretlendiğini, bu yüzden idleConnections: true eklemek gerektiğini belirtir. Çıktıdaki msg alanı, o anki aşamanın yüzde kaçının tamamlandığını gösterir.

javascript
db.getSiblingDB("admin").aggregate( [
   { $currentOp: { idleConnections: true } },
   { $match: { "command.createIndexes": { $exists: true } } }
] )

Replica set üzerinde index

Replica set ve sharded cluster ortamlarında index, veri taşıyan bütün üyelerde aynı anda kurulur. Primary, index hazır sayılmadan önce belirli sayıda oy veren üyenin işi bitirmesini bekler; bu sayıya commit quorum denir ve varsayılan değeri "votingMembers", yani veri taşıyan bütün oy veren üyelerdir. Belgeler, bu varsayılanla bir üye erişilemez hale gelirse index oluşturmanın o üye geri gelene kadar asılı kalabileceği konusunda uyarır. Gerekirse üçüncü parametre olarak "majority" veya bir sayı verebilirsiniz. Replica set kurulumunu replica set kurulumu yazısında anlattık.

javascript
db.urunler.createIndex( { stokKodu: 1 }, {}, "majority" )

Kesintilere karşı davranış da ortama göre değişir. Tek sunuculu (standalone) bir mongod index sırasında kapanırsa bütün ilerleme kaybolur ve yeniden başlatma indexi kendiliğinden sürdürmez; komutu tekrar çalıştırmanız gerekir. Replica set üyesi temiz biçimde kapanırsa ve commit quorum varsayılandaysa ilerleme diske yazılır ve yeniden başlatmada kaldığı yerden devam eder; çökme durumunda ise baştan başlar. Süren bir index oluşturmayı iptal etmek için dropIndex() veya dropIndexes() kullanılır; belgeler replica set ve sharded cluster ortamında bunun için killOp kullanılmamasını söyler.

Index silme: dropIndex ve dropIndexes

Performansa zarar veren, yenisiyle değiştirilecek ya da artık kullanılmayan bir indexi silebilirsiniz. Silmek için indexin adını bilmeniz gerekir; adı getIndexes() çıktısından alırsınız. Tek bir index için dropIndex(), birden çok index için ad dizisiyle dropIndexes(), _id dışındaki bütün indexler için de parametresiz dropIndexes() kullanılır.

javascript
// Tek index
db.urunler.dropIndex( "kategori_fiyat_liste" )

// Birden çok index
db.urunler.dropIndexes( [ "kategori_1", "stokKodu_1" ] )

// _id dışındaki bütün indexler
db.urunler.dropIndexes()

Komut, işlemin durumunu bildiren bir belge döndürür. Belgelerdeki örnekte bu belge { nIndexesWas: 3, ok: 1 } biçimindedir; nIndexesWas silme öncesindeki index sayısını gösterir. Silmenin gerçekleştiğini doğrulamak için getIndexes() komutunu yeniden çalıştırırsınız.

Bir indexi değiştirmek

mongosh'ta mevcut bir indexin seçeneklerini doğrudan değiştiremezsiniz; indexi silip yeniden oluşturmanız gerekir. İstisnalar şunlardır: hidden seçeneği hideIndex() ve unhideIndex() ile değişir, TTL indexlerin süresi collMod komutuyla güncellenir ve normal bir index yine collMod ile unique hale getirilebilir. Silip yeniden oluşturma arasında sorguların indexsiz kalmaması için belgeler geçici bir yedek index kullanmayı önerir:

javascript
// 1. Asıl index
db.ziyaretler.createIndex( { url: 1 } )

// 2. Aynı alanı içeren geçici index
db.ziyaretler.createIndex( { url: 1, gecici: 1 } )

// 3. Asıl indexi sil ve unique olarak yeniden oluştur
db.ziyaretler.dropIndex( "url_1" )
db.ziyaretler.createIndex( { url: 1 }, { unique: true } )

// 4. Geçici indexi kaldır
db.ziyaretler.dropIndex( "url_1_gecici_1" )

Bu sırada sorgular önce geçici indexi, ardından yeni unique indexi kullanır; uygulama hiçbir anda indexsiz kalmaz. Geçici indexin ilk alanı asıl indexle aynı olduğu için, compound index önek (prefix) kuralı sayesinde aynı sorguları destekleyebilir.

Sınırlar ve index kullanımını ölçmek

MongoDB belgelerindeki sınırlar sayfasına göre bir koleksiyonda en fazla 64 index bulunabilir ve bir compound index en fazla 32 alan içerebilir. Bu sınırlara yaklaşıyorsanız genellikle sorun index tasarımındadır: aynı öneki paylaşan gereksiz indexler veya hiç kullanılmayan eski indexler birikmiş olabilir.

Hangi indexin ne kadar kullanıldığını $indexStats aggregation aşamasıyla görebilirsiniz. Belgelere göre bu aşama erişim sayılarını yalnızca sorgunun çalıştığı düğüm için raporlar; tam resim için kümedeki her düğümde ayrı çalıştırmanız gerekir. Index boyutları için collStats çıktısındaki totalIndexSize ve indexSizes, veritabanı düzeyinde ise dbStats çıktısındaki indexes ve indexSize alanlarına bakılır.

javascript
db.urunler.aggregate( [ { $indexStats: { } } ] )

Sık yapılan hatalar

  • Her alana ayrı index açmak: MongoDB çoğu sorgu için tek bir index kullanır; birden çok alanı birlikte filtreliyorsanız ayrı ayrı tek alanlı indexler yerine uygun bir compound index gerekir.
  • Gömülü belgenin tamamını indexleyip iç alanı sorgulamak: { adres: 1 } indexi "adres.il" sorgularına yardım etmez; iç alanı nokta gösterimiyle indexleyin.
  • Seçenek değiştirmek için createIndex'i yeniden çalıştırmak: Aynı anahtarlarla farklı seçenek verdiğinizde mevcut index değişmez; silip yeniden oluşturun veya ilgili komutu kullanın.
  • Yoğun saatte büyük koleksiyona index eklemek: Bakım penceresi seçin, disk ve belleği önceden kontrol edin.
  • Kullanılan indexi doğrudan silmek: Önce gizleyip etkisini gözlemleyin.
  • Index etkisini ölçmeden varsaymak: Değişiklikten önce ve sonra explain çıktısını karşılaştırın.

Sık Sorulan Sorular

createIndex komutunu iki kez çalıştırırsam ne olur?

Aynı tanıma sahip bir index varsa MongoDB yeniden oluşturmaz; komut zararsızdır. Aynı adla farklı tanımda bir index oluşturmaya çalışırsanız ise hata alırsınız.

Index oluşturulurken koleksiyon kilitlenir mi?

Yalnızca başta ve sonda kısa süreli kilit alınır; aradaki tarama aşamasında okuma ve yazmalar devam eder. Yine de yoğun yazma altında index kurmak performansı düşürür, bu yüzden sakin bir zaman seçmek iyi olur.

Bir indexin adını değiştirebilir miyim?

Hayır. Belgelere göre index adı oluşturulduktan sonra değiştirilemez; indexi silip name seçeneğiyle yeniden oluşturmanız gerekir.

Indexin kullanılıp kullanılmadığını nasıl anlarım?

Tek bir sorgu için explain("executionStats") çıktısında IXSCAN aşamasını arayın; koleksiyon genelinde ise $indexStats ile erişim sayılarına bakın. Yavaş sorguları toplu yakalamak için veritabanı profiler kullanılabilir.

_id indexini silebilir veya gizleyebilir miyim?

Hayır. _id indexi silinemez ve gizlenemez; tek yol koleksiyonu tamamen silmektir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: MongoDB öğrenme rehberi · Compound index ve ESR kuralı · Unique, partial ve sparse index · TTL index · explain() ile sorgu planı · Yavaş sorgu profiler · E11000 duplicate key hatası