MongoDB’de Index Kullanımı ve Sorgu Optimizasyonu (MongoDB Öğreniyoruz 5)

Murat Çabuk
Kodcular
Published in
10 min readAug 8, 2022

--

Standart SQL veritabanlarında olduğu gibi document DB’lerde de index yönetimi çok önemli bir konu. Bu yazımızda MongoDB’de indeksleri yakından inceleyip sorgularımızın sonucu daha hızlı alabilmek için optimizasyon tekniklerini de öğrenceğiz.

Makale serisinin diğer yazıları için alttaki linkleri kullanabilirsiniz.

Bu konu için bol miktarda dokümana ihtiyacımız olacak. Bunun için Kaggle’daki “Books with ISBN of different genres” başlıklı veriyi kullanacağız. Eğer verilere ulaşmazsanız ya da ulaşsanız bide değişmesi durumunda hata alırsanız Github sayfamdaki repo’dan erişebilirsiniz.

Zip dosyasını indirdikten sonra extract yaptığınızda csv ve json dosyasını göreceksiniz. Biz csv olanı kullanacağız. Import etmek için attaki komu kullanabilirsiniz.

mongoimport "mongodb://localhost:27017/mymongodb" --collection kitaplar --type csv --file 'books.csv'  -u "root" -p "password123" --authenticationDatabase "admin" --fields=title,description,gender,num_pages,editorial,isbn,year_edition,date_edition,writer,image

Ayrıca örneklerimizde kullanabilmek için array, embedded document ve referenced document ekleyelim. Daha önce de dediğimiz gibi MongoDB Shell NodeJS ile tam uyumludur. Bu yeteneğinden faydalanarak

var degerler = [1,1,2,3,4,5,6,7,8,9]
var kategoriler = ['gezi','roman','tarih', 'felsefe','bilim','sanat','magazin','teknoloji','spor', 'sağlık']function random(){return Math.floor(Math.random()*10)}db.yayinevleri.insertMany( [
{ _id: 1, item: "A Yayınevi", adres: "istanbul" },
{ _id: 2, item: "B Yayınevi", adres: "izmir" },
{ _id: 3, item: "C Yayınevi", adres: "ankara" },
{ _id: 4, item: "D Yayınevi", adres: "istanbul"},
{ _id: 5, item: "E Yayınevi", adres: "samsun" },
{ _id: 6, item: "F Yayınevi", adres: "mardin"},
{ _id: 7, item: "G Yayınevi", adres: "antalya"},
{ _id: 8, item: "H Yayınevi", adres: "samsun" },
{ _id: 9, item: "I Yayınevi", adres: "mardin"}])
var deger=[]
var kategori = {}
var yayinevi=[]
var kitaplar = db.kitaplar.find()kitaplar.forEach(

function(kayit){
deger=[]
yayinevi=[]
kategori={}
for (i=0;i<3;i++){
kategori['kat'+ i]=kategoriler[random()]
deger.push(degerler[random()])
yayinevi.push(degerler[random()])
}
db.kitaplar.update({_id:kayit._id},{$set:{'degerler':deger,'kategoriler':kategori, 'yayinevleri':yayinevi}})
})

Document sayısına bakalım.

db.kitaplar.countDocuments()//31285

Kitaplar collection’ımızın alanlarını inceleyelim.

  • title: Kitabın adı
  • description: Özet
  • gender: Kitabın kategorisi
  • num_pages: Sayfasayısı
  • editorial: Yayınevi
  • isbn: Seri no
  • year_edition: Yayınlanma yılı
  • date_edition: Yayınlanma tarihi
  • writer: Yazar
  • image: Kapak resmi

Örnek bir doküman:

{
_id: ObjectId("62d2cc512fda470ae850d596"),
title: 'SINSONTE',
description: 'Con ecos deFahrenheit 451, Un mundo feliz oBlade Runner, Sinsonte es una de las novelas de ciencia ficción más míticas de nuestro tiempo, que se lee como una elegía a los olvidados ...',
num_pages: 360,
editorial: 'IMPEDIMENTA',
isbn: Long("9788418668371"),
year_edition: 2022,
date_edition: '04/04/2022',
writer: 'Walter Tevis',
image: 'https://imagessl1.casadellibro.com/a/l/t5/71/9788418668371.jpg',
degerler: [ 6, 2, 1 ],
kategoriler: { kat0: 'gezi', kat1: 'spor', kat2: 'roman' },
yayinevleri: [ 2, 6, 7 ]
}

Bu arada bu veriler gerçekten var. Yani kitabı satan websitesi https://www.casadellibro.com adresinden hakikaten bu kitapları ve detaylarını görebilirsiniz. Hatta kapak resmilerinin adresleri bile çalışıyor.

Bizim eklediğimiz alanlar ise sadece test amaçlı, index eklemden önce ve sonraki durumları anlamak için kullanacağız.

Artık çalışmaya hazırız.

Single Filed Index

Amacımız sayfa sayılarına göre kitapları filtrelemek olsun. Toplamda 31285 kitap vardı. Sayfa sayısı 100 den fazla 14159 kitap varmış.

db.kitaplar.find({num_pages:{$gt:100}}).count()//14159

Şimdi hiç index kullanmadan sayfa sayısı 100 den büyük olan dokümanların listesini alırken MongoDb’nin nasıl bir yol izlediğini, kaç adet doküman taradığını inceleyelim.

Bunu için cursor metotlarından biri olan cursor.explain() metodunu kullanıyor olacağız. Yukarıda verilerimiz ayarlarken cursor.forEach() metodunu da kullanmıştık. Bütün metotları incelemek için şu sayfayı ziyaret ediniz.

Explain metodunu sonuçlarını executionStats olarak istediğimizi belirtiyoruz. Böylece sistemin çalışm aşekli hakkında bazı bilgiler edinmiş olacağız.

Sonuçlaı inceleyeccek olursak

  • nReturned: 14159 ile 14159 satır döndüğünü,
  • executionTimeMillis: 43 ile 43 milisaniyede işin tamamlandığını,
  • totalDocsExamined: 31285 ile 31285 adet dokümanın tarandığını,
  • stage: ‘COLLSCAN’ ile Collection Scan yapıldığını

anlayabiliyoruz.

Diğer stage türleri de şu şekilde

  • COLLSCAN : collection taraması (collection scan)
  • IXSCAN : index taraması (index scan)
  • FETCH : Dokümanları almak için
  • SHARD_MERGE : Shard üzerinden gelen verilerin merge edilmesi için
  • SHARDING_FILTER : Shard üzerindeki sahipsiz dokümanları filtrelemek için
db.kitaplar.find({num_pages:{$gt:100}}).explain("executionStats")//  executionStats: {
// nReturned: 14159,
// executionTimeMillis: 43,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,
// executionStages: {
// stage: 'SINGLE_SHARD',
// nReturned: 14159,
// executionTimeMillis: 43,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,
// totalChildMillis: Long("42"),
// shards: [
// {
// shardName: 'shard2',
// executionSuccess: true,
// nReturned: 14159,
// executionTimeMillis: 42,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,
// executionStages: {
// stage: 'COLLSCAN',
// filter: { num_pages: { '$gt': 100 } },
// nReturned: 14159,
// executionTimeMillisEstimate: 4,
// works: 31287,
// advanced: 14159,
// needTime: 17127,
// needYield: 0,
// saveState: 31,
// restoreState: 31,
// isEOF: 1,
// direction: 'forward',
// docsExamined: 31285
// }
// }
// ]
// }
// },

Şimdi sayfa sayısı sütunu için index atıp bir de arama sonucumu öyle değerlendirelim.

num_pages için yazdığımız 1 değeri indekslemenin artan (asc) olarak yapılmasını istediğimizi belirtiyor. -1 yazsaydık azalan olarak yapacaktı.

db.kitaplar.createIndex( { num_pages: 1 } )

Bir de şimdi bakalım executionStats değerlerine. Benim kullandığım MongoDB sharded cluster olduğu için sizinkinden farklı sonuçlar içeriyor olabilir ancak inceleyeceğimiz kısımlar ortak olacak.

  • totalDocsExamined: 14159: artık bütün dokümanları taramadığını görebiliyoruz.
  • executionTimeMillis: 37 : süre de kısalmış 37 milisaniye
  • inputStage altındaki stage: ‘IXSCAN’: Artık index scan yapıldığını gösteriyor.
db.kitaplar.find({num_pages:{$gt:100}}).explain("executionStats")//  executionStats: {
// nReturned: 14159,
// executionTimeMillis: 37,
// totalKeysExamined: 14159,
// totalDocsExamined: 14159,
// executionStages: {
// stage: 'SINGLE_SHARD',
// nReturned: 14159,
// executionTimeMillis: 37,
// totalKeysExamined: 14159,
// totalDocsExamined: 14159,
// totalChildMillis: Long("32"),
// shards: [
// {
// shardName: 'shard2',
// executionSuccess: true,
// nReturned: 14159,
// executionTimeMillis: 32,
// totalKeysExamined: 14159,
// totalDocsExamined: 14159,
// executionStages: {
// stage: 'FETCH',
// nReturned: 14159,
// executionTimeMillisEstimate: 5,
// works: 14160,
// advanced: 14159,
// needTime: 0,
// needYield: 0,
// saveState: 14,
// restoreState: 14,
// isEOF: 1,
// docsExamined: 14159,
// alreadyHasObj: 0,
// inputStage: {
// stage: 'IXSCAN',
// nReturned: 14159,
// executionTimeMillisEstimate: 0,
// works: 14160,
// advanced: 14159,
// needTime: 0,
// needYield: 0,
// saveState: 14,
// restoreState: 14,
// isEOF: 1,
// keyPattern: { num_pages: 1 },
// indexName: 'num_pages_1',
// isMultiKey: false,
// multiKeyPaths: { num_pages: [] },
// isUnique: false,
// isSparse: false,
// isPartial: false,
// indexVersion: 2,
// direction: 'forward',
// indexBounds: { num_pages: [ '(100, inf.0]' ] },
// keysExamined: 14159,
// seeks: 1,
// dupsTested: 0,
// dupsDropped: 0
// }
// }
// }
// ]
// }
// },

Kitaplar collection’ınında sayfa sayısı 0 olan kayıtların sayısına bakalım.

db.kitaplar.find({num_pages:0}).count()//16414

Yani sayfa sayısı -1'den daha büyük kayıtları getir dersek bütün kayıtların gelmesi lazım. Şimdi bu tarz bi sorguyu index varken yapıp istatistikleri inceleyelim.

db.kitaplar.find({num_pages:{$gt:-1}}).explain("executionStats")// nReturned: 31284,
// executionTimeMillis: 95,
// totalKeysExamined: 31284,
// totalDocsExamined: 31284,

Executiontime 95 milisaniye ve bütün dokümanlar taranmış.

Şimdi indeksi silip bir de öyle deneyelim.

db.kitaplar.getIndexes()
// [
// { v: 2, key: { _id: 1 }, name: '_id_' },
// {
// v: 2,
// key: { num_pages: 1 }, ----------------------> bunu sileceğiz
// name: 'num_pages_1',
// background: false
// }
// ]
db.kitaplar.dropIndex( { "num_pages": 1 } )db.kitaplar.find({num_pages:{$gt:-1}}).explain("executionStats")// nReturned: 31284,
// executionTimeMillis: 53,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,

Dikkat ederseniz indeks olmayan bir collection’da bütün dokümanları gezmeniz gereken bir sorgu veya ona yakın bir sorgu çalıştırdığımızda indeks oluşturulmuş collection’dan daha hızlı sonuç dönmektedir.

Bunun nedeni ise MongoDB için bu tarz bir sorguda indeksi ekstra step olarak kullanması. Yani normalde bütün dokümanları dolaşmak için MongoDB doğrudan memory üzerinden sorguyu yapabilecekken indeks üzerinde gittiğinde verileri tekrar çekip indeks verileri üzerinde filtreleme yapmaya çalışıyor olmasından dolayı bu tarz sorgularda index daha yavaş çalışıyor.

Compound Index vs Sıralama (Sorting)

Örnek verimiz:

{
_id: ObjectId("62d2cc512fda470ae850d596"),
title: 'SINSONTE',
description: 'Con ecos deFahrenheit 451, Un mundo feliz oBlade Runner, Sinsonte es una de las novelas de ciencia ficción más míticas de nuestro tiempo, que se lee como una elegía a los olvidados ...',
num_pages: 360,
editorial: 'IMPEDIMENTA',
isbn: Long("9788418668371"),
year_edition: 2022,
date_edition: '04/04/2022',
writer: 'Walter Tevis',
image: 'https://imagessl1.casadellibro.com/a/l/t5/71/9788418668371.jpg',
degerler: [ 6, 2, 1 ],
kategoriler: { kat0: 'gezi', kat1: 'spor', kat2: 'roman' },
yayinevleri: [ 2, 6, 7 ]
}

Sayfa sayısı ve yıl için bir indeks oluşturalım. Ancak oluşturmadan önce bir sorgu çekelim.

db.kitaplar.find({year_edition:{$gt:2000}, num_pages:{$gt:200}}).explain("executionStats")// nReturned: 9130,
// executionTimeMillis: 31,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,

Bütün dokümanlar gezilmiş ve 40 milisaniye sürmüş.

db.kitaplar.createIndex( { "year_edition": 1, "num_pages": 1 })// nReturned: 9130,
// executionTimeMillis: 24,
// totalKeysExamined: 9154,
// totalDocsExamined: 9130,

Compound Index’lerde dikkaet edilmesi gereken konulardan biri index oluşturulurken sırasıyla verilen alanlardan ilk (bizde year_edition) alan da tek başına indekslenmiştir.

Yani tek başına year_edition alanı için sorgu çektiğimizde de index scan yapıldığını görebiliriz. Ancak num_pages için sorgu çalıştırdığımızda collection scan yapılacaktır.

Indekslerin bir güzel tarafı da sıralamada kullanılabilmesidir. Sıralama performansını ciddi ölçüde arttırır.

db.kitaplar.find().sort({year_edition:1, num_pages:1}).explain("executionStats")
// bunun sonucunda index scan yapılır çünkü indexe uygun sıralama yaptık.
db.kitaplar.find().sort({year_edition:1, num_pages:-1}).explain("executionStats")
// bunun sonucu collection scan olacaktır çünkü num_pages alanını tersten sıraladık.

Multikey Index

Array alanlar üzerinde oluşturduğumuz indeksler birer multikey indekstir. Örneğin kitaplar collection’ımıza eklediğimiz degerler alanı için index oluşturduğumuzda array’in bütün değerleri için indeks oluşturulur.

Bounds kavramı ise oluşturulan indeksin sınırlarını belirler. Örneğin [ [ 3, Infinity ] ] ve [ [ -Infinity, 6 ] ] için alt sınır 3 ve üst sınır 6'dır.

// doküman
{ _id: 1, item: "ABC", ratings: [ 2, 9 ] }
{ _id: 2, item: "XYZ", ratings: [ 4, 3 ] }
// multikey index
db.survey.createIndex( { ratings: 1 } )
// sorgu
db.survey.find( { ratings : { $elemMatch: { $gte: 3, $lte: 6 } } } )

Sorgu ($elemMatch) şu şekilde çalışır 3 ve 6 değerlerinin aynı anda sağlayan dokümanları döndürür.

Ancak soru alttaki gibi değişirse şartlardan her hangi birini sağlayan doküman alınır. Yani rating değerinde 3'e eşit veya daha büyük değer olan dokümanlarla, 6'ya eşit veya daha küçük değere sahip olan dokümanlar alınır.

db.survey.find( { ratings : { $gte: 3, $lte: 6 } }  )

Bizde değerler alanına göre index oluşturalım. Ama önce indeks olmadan bir filtre çalıştıralım.

db.kitaplar.find({degerler:{$gt:2,$lt:6}}).explain("executionStats")// nReturned: 28425,
// executionTimeMillis: 80,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,
// executionStages: {
// stage: 'COLLSCAN',

Şimdi indeks oluşturup tekrar deneyelim.

db.kitaplar.createIndex( { degerler: 1 } )db.kitaplar.find({degerler:{$gt:2,$lt:6}}).explain("executionStats")// nReturned: 28425,
// executionTimeMillis: 77,
// totalKeysExamined: 59343,
// totalDocsExamined: 30449,
// inputStage: {
// stage: 'IXSCAN',

Multikey index’i embedded dokümanlara da uygulamak mümkün.

{
_id: ObjectId("62d2cc512fda470ae850d596"),
title: 'SINSONTE',
description: 'Con ecos deFahrenheit 451, Un mundo feliz oBlade Runner, Sinsonte es una de las novelas de ciencia ficción más míticas de nuestro tiempo, que se lee como una elegía a los olvidados ...',
num_pages: 360,
editorial: 'IMPEDIMENTA',
isbn: Long("9788418668371"),
year_edition: 2022,
date_edition: '04/04/2022',
writer: 'Walter Tevis',
image: 'https://imagessl1.casadellibro.com/a/l/t5/71/9788418668371.jpg',
degerler: [ 6, 2, 1 ],
kategoriler: { kat0: 'gezi', kat1: 'spor', kat2: 'roman' },
yayinevleri: [ 2, 6, 7 ]
}

Mesela kategoriler üzerinde uygulayabiliriz.

db.kitaplar.find({'kategoriler.kat0':'spor'}).explain("executionStats")// nReturned: 3077,
// executionTimeMillis: 56,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,
// executionStages: {
// stage: 'COLLSCAN',
// executionTimeMillisEstimate: 7,
db.kitaplar.createIndex( { "kategoriler.kat0": 1 } )// nReturned: 3077,
// executionTimeMillis: 10,
// totalKeysExamined: 3077,
// totalDocsExamined: 3077,
// executionTimeMillisEstimate: 3,

Text Index ve Text Search

Index tiplerinde biri de text. Eğer özellikle bir text alanı için indeks oluştururken index tipini text olarak belirtmezsek aramalarda ilgili text’i full vermemiz veya regular expression gibi test içinde arama yapabilen fonksiyonlar kullanmamız gerekir.

Ayrıca text index bir metindeki bütün kelimeleri tek tek indeksler. Eğer arama kısmına iki kelime yazarsak bunları ayrı ayrı OR kullanarak arar. Yazdığımız kelimeleri olduğu gibi aramasını istiyorsak kelimenin başına ve sonuna tırnakların içine bölünü eklemliyiz. Alttaki örnekte açıklamada var.

//regex arama
db.kitaplar.find({description:{$regex : "que"}}).explain("executionStats")
// nReturned: 12986,
// executionTimeMillis: 45,
// totalKeysExamined: 0,
// totalDocsExamined: 31285,
// executionTimeMillisEstimate: 8,
// eğer iki kelimeyi olduğu gibi aramasını isteseydik alttaki gibi yazmalıydık. buna phrase arama deniliyor
//db.kitaplar.find({description:{$regex : "\"que se\""}}).explain("executionStats")
//text index ve aramadb.kitaplar.createIndex({ description: "text" })
db.kitaplar.find({$text:{$search : "que"}}).explain("executionStats")// nReturned: 10749,
// executionTimeMillis: 32,
// totalKeysExamined: 10749,
// totalDocsExamined: 10749,
// executionStages: {
// stage: 'TEXT_MATCH',
// executionTimeMillisEstimate: 7,

Birden fazla alan eklense bile buradaki kelimler merge edilerek tek olarak arama yapılır. Ancak birden fazla alan eklendiğinde alanlara ağırlık vermek mümkün.

Öncelikle öncekli indeksi silelim.

db.kitaplar.getIndexes()//{
// v: 2,
// key: { _fts: 'text', _ftsx: 1 },
// name: 'description_text',
// weights: { description: 1 },
// default_language: 'english',
// language_override: 'language',
// textIndexVersion: 3
//}
// key alanını kullanarak silme yapılabilirdb.kitaplar.dropIndex({ _fts: 'text', _ftsx: 1 })

Yeni indeksi oluşturalım.

db.kitaplar.createIndex(
{
title: "text",
description: "text"
},
{
weights: {
title: 10,
description: 5
},
name: "TextIndex"
}
)

Text search yaparken biz belirtmediğimiz sürece aramalar İngilizce dili yapısına göre yapılır. Ancak gördüğümüz üzere bizim kitaplar collection’ınımın dili İspanyolca. Text tipinde index oluştururken dil de belirtilebilir. Örnek olarak aşağıdaki gibi index detaylarına dili de ekliyoruz.

db.quotes.createIndex(
{ content : "text" },
{ default_language: "spanish" }
)

Index Özellikleri

  • Unique Index: Oluşturulacak index unique olacaksa bu özellik kullanılır. Yani index oluşturduğumuz değer tüm dokümanlarda tekilse (unique) bu kullanılır.

Örneğin,

db.members.createIndex( { "user_id": 1 }, { unique: true } )
  • Partial Index: Dokümanların bazılarını indekslemek istediğimizde kullanabiliriz.

Örneğin, rating alanın değeri 5'den büyük olan dokümanlarını indekslemesini sağlamak için alttaki gibi bir index oluşturulmalıdır.

db.restaurants.createIndex(
{ cuisine: 1, name: 1 },
{ partialFilterExpression: { rating: { $gt: 5 } } }
)

Case Sensitive Index

Collation özelliğini kullanarak büyük küçük harf duyarlı index oluşturulabilir.

  • locale: Dil ayarlanır. Detayları için şu sayfayı ziyaret ediniz.
  • strength :

1 = Büyük küçük harfı yok sayar.

2 = Temel karakterlerin (birincil farklılıklar) ve aksanların (ikincil farklılıklar) karşılaştırmalarını gerçekleştirir. Temel karakterler arasındaki farklar, ikincil farklılıklara göre önceliklidir.

Diğerleri için şu sayfayı ziyaret ediniz.

db.collection.createIndex( { "key" : 1 },
{ collation: {
locale : <locale>,
strength : <strength>
}
} )

Indeksleri Yönetmek

  • Herhangi bir veritabanındaki bütün indeksleri listelemek için
db.getCollectionNames().forEach(function(collection) {
indexes = db[collection].getIndexes();
print("Indexes for " + collection + ":");
printjson(indexes);
});
  • Herhangi bir indeksi silmek için

Index listesinden alacağımız index key’ini fonksiyona veriyoruz.

db.accounts.dropIndex( { "tax-id": 1 } )
  • Bütün indeksleri silmek için
db.accounts.dropIndexes()

Indeksi Zorunlu Kılmak

Oluşturduğumuz indeksi sorguda zorunlu tutmak için.

db.people.find(
{ name: "John Doe", zipcode: { $gt: "63000" } }
).hint( { zipcode: 1 } ).explain("executionStats")

Son olarak Index’lerin ram’e sığıyor olması çok önemli böylece indekslerin diskten okunmasına gerek kalmaz. Bunun için alttaki komutla oluşturduğunuz indeksin byte olarak boyutunu kontrol ediniz.

db.collection.totalIndexSize()
//4617080000

Ayrıca alttaki komutla sonuçlarda ilgili collection’ın kaç adet indekse sahip olduğunu ve ram’de nekadar yer turruğunu görebilirsiniz.

  • totalIndexSize
  • nindexes
db.collection.stats()

Son olarak MongoDB schema design üzerine kurulu “Learn MongoDB The Hard Way” sayfasını ziyaret etmenizi tavsiye ederim.

Umarım faydalı olmuştur.

Makale serisinin diğer yazıları için alttaki linkleri kullanabilirsiniz.

Kaynaklar

Originally published at https://github.com.

--

--