MongoDB aggregation pipeline, belgeleri art arda dizilmiş aşamalardan (stage) geçirerek işleyen bir veri işleme yöntemidir. Her aşama gelen belgeler üzerinde bir iş yapar (filtreler, şekillendirir, sıralar, gruplar) ve çıktısını bir sonraki aşamaya verir. db.collection.aggregate([ ... ]) ile çalıştırılır ve sonuç bir cursor olarak döner. MongoDB belgeleri aggregation için tercih edilen yöntemin pipeline olduğunu belirtir.

Bu yazı pipeline'ın temel mantığını, en sık kullanılan $match, $project, $sort, $limit ve $skip aşamalarını, MongoDB'nin pipeline'ı nasıl otomatik iyileştirdiğini ve bellek sınırlarını anlatır. Gruplama için $group ve accumulator, koleksiyon birleştirme için $lookup yazılarına, serinin tamamı için MongoDB öğrenme rehberine bakabilirsiniz.

Pipeline nasıl çalışır?

Bir pipeline bir veya daha fazla aşamadan oluşan bir dizidir. Belgeler bir koleksiyondan, bir view'dan veya özel olarak tasarlanmış bir aşamadan gelebilir. Her aşama bir belge akışı alır, onu işler ve yeni bir akış üretir. Bir aşamanın her giriş belgesi için bir çıktı belgesi üretmesi gerekmez; bazı aşamalar belge filtreler, bazıları yeni belgeler üretir. Aynı aşama bir pipeline'da birden fazla kez kullanılabilir; istisnalar $out, $merge ve $geoNear aşamalarıdır.

Önemli bir güvence: aggregate() ile çalıştırılan pipeline'lar, içinde $merge veya $out aşaması yoksa koleksiyondaki belgeleri değiştirmez. Yani bir pipeline'ı denerken veriniz güvendedir; sonuçları bir koleksiyona yazmak istediğinizde bu iki aşamaya geçersiniz. Ayrıntılar $out, $merge ve materialized view yazısında.

javascript
db.siparisler.insertMany([
  { _id: 1, musteri: "ayse", sehir: "İzmir", durum: "teslim", tutar: 250, urunler: 3 },
  { _id: 2, musteri: "mehmet", sehir: "Ankara", durum: "iptal", tutar: 90, urunler: 1 },
  { _id: 3, musteri: "zeynep", sehir: "İzmir", durum: "teslim", tutar: 480, urunler: 5 },
  { _id: 4, musteri: "can", sehir: "Bursa", durum: "teslim", tutar: 130, urunler: 2 },
  { _id: 5, musteri: "ayse", sehir: "İzmir", durum: "beklemede", tutar: 60, urunler: 1 }
])

db.siparisler.aggregate([
  { $match: { durum: "teslim" } },                    // 1) filtrele
  { $project: { _id: 0, musteri: 1, sehir: 1, tutar: 1,
               ortalamaUrun: { $divide: ["$tutar", "$urunler"] } } },  // 2) şekillendir
  { $sort: { tutar: -1 } },                           // 3) sırala
  { $limit: 2 }                                       // 4) ilk ikiyi al
])

Bu pipeline önce yalnızca teslim edilmiş siparişleri (1, 3 ve 4) geçirir, sonra her belgeden müşteri, şehir ve tutar alanlarını alıp ürün başına ortalama tutarı hesaplayan yeni bir alan ekler, tutara göre azalan sıralar ve ilk iki belgeyi döndürür. Sonuç, zeynep (480) ve ayse (250) siparişlerine ait iki belgedir. Aşamaların sırası sonucu ve maliyeti doğrudan etkiler; bunu aşağıdaki optimizasyon bölümünde ayrıntılı göreceğiz.

İfadeler ve alan yolları

Birçok aşama ifade (expression) kabul eder. İfadeler sabitlerden (3), operatörlerden ($add, $divide gibi) ve alan yolu ifadelerinden oluşur. Alan yolu, giriş belgesindeki bir alana erişmek için alan adının başına $ eklenerek yazılır: "$tutar" tutar alanını, "$adres.sehir" gömülü belgedeki şehir alanını gösterir. Örneğin { $add: [ 3, "$stok.toplam" ] } ifadesi, belgedeki stok.toplam değerine 3 ekler.

Bu ayrım yeni başlayanların en sık takıldığı noktadır: $match içinde { tutar: 100 } normal bir sorgu filtresidir, alan adı dolar işaretsiz yazılır. $project veya $group içindeki ifadelerde ise alanın değerine erişmek için "$tutar" yazmanız gerekir. Aşamaların alan ekleme ve hesaplama ayrıntıları için $addFields, $set ve $project aşamaları yazısına bakın.

Temel aşamalar

Temel aşamalar: $match, $project, $sort, $limit ve $skip
AşamaGöreviDikkat edilecek nokta
$matchBelgeleri sorgu koşuluna göre filtrelerfind ile aynı söz dizimi; mümkün olduğunca başa koyun
$projectAlanları seçer, gizler, yeniden adlandırır, hesaplarHariç tutma ile dahil etme karıştırılamaz (_id hariç)
$sortBelgeleri sıralarEngelleyici (blocking) aşama, en fazla 32 anahtar
$limitSonraki aşamaya geçen belge sayısını sınırlarPozitif 64 bit tamsayı alır
$skipBaştan belirtilen sayıda belgeyi atlarSayfalamada $sort ile birlikte kullanılır
$groupBelgeleri anahtara göre gruplarAyrıntılar $group yazısında
$lookupBaşka koleksiyonla birleştirirAyrıntılar $lookup yazısında

$match

$match belgeleri bir sorgu koşuluna göre filtreler ve söz dizimi find() sorgu parametresiyle aynıdır. Belgelerin iki net önerisi vardır: $match'i pipeline'da olabildiğince erken konumlandırın, çünkü sonraki aşamalara giden belge sayısını azaltır; ve pipeline'ın en başına koyarsanız sorgu, tıpkı bir find() gibi indekslerden yararlanabilir. Sorguda aggregation ifadesi kullanmak için $expr gerekir. Kısıtlamalar da vardır: $match içinde $where kullanılamaz; $near ve $nearSphere yerine $geoNear aşaması veya $geoWithin kullanılmalıdır; $text kullanılacaksa $match pipeline'ın ilk aşaması olmalıdır.

javascript
// İki alanı birbiriyle karşılaştırmak için $expr
db.siparisler.aggregate([
  { $match: { $expr: { $gt: ["$tutar", { $multiply: ["$urunler", 50] }] } } }
])

$project

$project sonraki aşamaya hangi alanların gideceğini belirler. alan: 1 dahil eder, alan: 0 hariç tutar, alan: ifade yeni bir alan ekler veya mevcut alanın değerini değiştirir. _id varsayılan olarak çıktıya dahildir; istemiyorsanız açıkça _id: 0 yazmalısınız. _id dışında bir alanı hariç tutarsanız aynı $project içinde başka bir biçim (dahil etme, yeni alan) kullanamazsınız. Sayısal veya mantıksal bir değeri sabit olarak atamak istiyorsanız $literal kullanın; aksi halde 1 veya true dahil etme bayrağı olarak yorumlanır. Var olmayan bir alanı dahil etmek o alanı eklemez, sessizce yok sayılır.

javascript
db.siparisler.aggregate([
  { $project: {
      _id: 0,
      kim: "$musteri",               // yeniden adlandırma
      tutar: 1,
      sabitPuan: { $literal: 1 }     // 1 değerini sabit olarak yaz
  } }
])

$sort, $limit ve $skip

$sort belgeleri bir veya daha fazla alana göre sıralar (1 artan, -1 azalan) ve çok alanlı sıralamada soldan sağa değerlendirir. Engelleyici (blocking) bir aşamadır: işlemeye başlamadan önce bütün girdinin gelmesini bekler; bu, paralel işlemeyi azaltır ve büyük veride çok bellek kullanabilir. Kararlı bir sıralama değildir, bu yüzden tutarlı sonuç için sıralamaya _id gibi benzersiz bir alan eklenmelidir. $limit geçen belge sayısını sınırlar ve belgelerin içeriğini değiştirmez; $skip baştan belge atlar. Find tarafındaki karşılıkları ve sayfalama için sort, limit, skip ve sayfalama yazısına bakın.

MongoDB pipeline'ı nasıl iyileştirir?

MongoDB pipeline'ı nasıl iyileştirir? Projeksiyon optimizasyonu, Projeksiyon + $match, Hesaplanan alan + $match, $sort + $match

Aggregation işlemlerinin bir optimizasyon aşaması vardır; MongoDB pipeline'ı daha iyi performans için yeniden şekillendirebilir. Bunun nasıl yapıldığını görmek için pipeline'ı explain ile çalıştırırsınız. Belgeler bu optimizasyonların sürümler arasında değişebileceğini de belirtir. Bilinen başlıca dönüşümler şunlardır:

  • Projeksiyon optimizasyonu: Pipeline sonucu üretmek için belgelerin yalnızca bir kısım alanına ihtiyaç duyuyorsa, yalnızca o alanları taşır ve akan veri miktarını azaltır.
  • Projeksiyon + $match: $project, $unset, $addFields veya $set aşamasından sonra gelen bir $match'teki, hesaplanan alanlara bağlı olmayan filtreler projeksiyonun önüne taşınır. Bir $match birden çok koşul içeriyorsa her koşul ayrı ayrı değerlendirilir ve bağımlı olmadığı projeksiyonların önüne geçirilir.
  • Hesaplanan alan + $match: Bir $set, $addFields veya $project aşamasının hesapladığı alan sonraki bir $match'te filtreleniyorsa ve hesaplama önceki aşamalara bağlı değilse, MongoDB hesaplamayı ve filtreyi birlikte daha öne, örneğin pahalı bir $lookup'ın önüne taşıyabilir. Bu, çıktı belgelerindeki alan sırasını değiştirebilir.
  • $sort + $match: Sıralamadan sonra gelen filtre sıralamanın önüne alınır; böylece daha az belge sıralanır.
  • $project veya $unset + $skip: $skip projeksiyonun önüne taşınır.
  • $sort + $limit birleşmesi: Arada belge sayısını değiştiren bir aşama ($unwind veya $group gibi) yoksa limit sıralamanın içine katılır; sıralama yalnızca ilk n sonucu bellekte tutar. Arada bir $skip varsa limit değeri skip kadar artırılır.
  • $limit + $limit, $skip + $skip, $match + $match: Art arda gelen iki limit küçük olanda, iki skip toplamda, iki match ise $and ile tek bir aşamada birleşir.
  • $lookup + $unwind (+ $match): $lookup'ın as alanını açan bir $unwind lookup'a katılır; ardından o alanın alt alanlarına bir $match geliyorsa o da katılır. Bu, büyük ara belgelerin oluşmasını önler.

Bu optimizasyonlar size kolaylık sağlar ama iyi bir pipeline yazma sorumluluğunu ortadan kaldırmaz. İyileştirici bir $match'i ancak hesaplanan alanlara bağlı değilse öne taşıyabilir; ilk aşamada indeks kullanılmasını garanti etmenin en kolay yolu, filtreyi zaten en başa yazmaktır.

İndeks kullanımı

Pipeline, giriş koleksiyonundaki indekslerden yararlanabilir. Belgelere göre $match, iyileştirme sonrasında pipeline'ın ilk aşamasıysa indeks kullanabilir. $sort, önünde $project, $unwind veya $group yoksa indeks kullanabilir. $group ise pipeline aynı alana göre sıralayıp grupluyorsa ve yalnızca $first veya $last kullanıyorsa her grubun ilk veya son belgesini bulmak için indeksten yararlanabilir. $geoNear her zaman bir coğrafi indeks kullanır. $lookup, $graphLookup ve $unionWith gibi başka koleksiyonlardan veri çeken aşamalar da o koleksiyonlardaki indeksleri kullanabilir.

Başta gelen $match ve ardından gelen $sort, tek bir sıralı sorguya eşdeğerdir ve indeks kullanabilir. Pipeline'ın indeks kullanıp kullanmadığını görmek için sorgu planında IXSCAN veya DISTINCT_SCAN planlarını ararsınız. Plan okuma explain ve sorgu planı, uygun indeks tasarımı compound index ve ESR kuralı yazılarında.

javascript
// Pipeline'ın nasıl çalıştırılacağını görmek
db.siparisler.explain("executionStats").aggregate([
  { $match: { durum: "teslim" } },
  { $sort: { tutar: -1 } },
  { $limit: 2 }
])

// Önerilen indeks: önce eşitlik, sonra sıralama alanı
db.siparisler.createIndex({ durum: 1, tutar: -1 })

Belgeler explain çıktısının insanlar için tasarlandığını ve biçiminin sürümler arasında değişebileceğini belirtir; bu çıktıyı programatik olarak ayrıştıran kodlar yazmaktan kaçının.

Sınırlar: belge boyutu, aşama sayısı, bellek

Aggregation işlemlerinin bilinmesi gereken birkaç sınırı vardır:

  • Sonuç belgesi boyutu: Sonuç kümesindeki her belge 16 MiB BSON belge sınırına tabidir. Sınır yalnızca döndürülen belgeler için geçerlidir; işleme sırasında ara belgeler bu boyutu aşabilir.
  • Aşama sayısı: Tek bir pipeline'da en fazla 1000 aşama olabilir; ayrıştırma öncesinde veya sonrasında bu sınır aşılırsa hata alırsınız.
  • Aşama başına bellek: 100 megabayttan fazla bellek gerektiren aşamaların davranışını allowDiskUseByDefault parametresi belirler. true ise bu aşamalar diske geçici dosya yazar; sorgu bazında { allowDiskUse: false } ile kapatılabilir. false ise hata verirler; sorgu bazında { allowDiskUse: true } ile açılabilir. Diske yazabilen aşamalara örnek olarak $group, $bucket, $setWindowFields, $sortByCount ve indeksle desteklenmeyen $sort verilir.
  • $push ve $addToSet: Bu accumulator'lar diske yazamaz; biriktirilen dizi sınırı aşarsa allowDiskUse açık olsa bile hata alınır.

Bellek hatası alırsanız belgelerin önerileri şunlardır: sıralamayı bir indeksle karşılayın ki bellekte sıralama yapılmasın, $sort aşamasından sonra bir $limit ekleyin ki sıralama yalnızca gereken belgeleri tutsun ve $match ile belgeleri olabildiğince erken filtreleyin.

javascript
db.siparisler.aggregate(
  [ { $sort: { tutar: -1 } } ],
  { allowDiskUse: true }      // 100 MB'ı aşan aşama diske yazabilsin
)

Cursor, transaction ve mongosh davranışı

aggregate() varsayılan olarak bir cursor döndürür; pipeline $out veya $merge içeriyorsa cursor boş döner, explain seçeneği verilmişse işlem ayrıntılarını içeren bir belge döner. mongosh, değişkene atanmamış bir aggregate cursor'ını otomatik olarak 20 kez yineler. Aggregation cursor'ları yalnızca değerlendirilmiş cursor'lar üzerinde çalışan yöntemleri destekler; hasNext(), next(), toArray(), forEach(), map(), itcount() bunlar arasındadır.

aggregate() transaction içinde kullanılabilir, ancak bazı aşamalar transaction'da yasaktır: $out, $merge, $collStats, $currentOp, $indexStats, $listLocalSessions, $listSessions ve $planCacheStats. Transaction içinde explain seçeneği de kullanılamaz. Ayrıca pipeline'ı dizi olarak değil ayrı argümanlar halinde verirseniz options parametresini kullanamazsınız; bu yüzden pipeline'ı her zaman dizi olarak yazmak iyi bir alışkanlıktır.

Tek amaçlı yöntemler ve map-reduce

Basit ihtiyaçlar için MongoDB tek amaçlı aggregation yöntemleri de sunar: estimatedDocumentCount(), count() ve distinct(). Bunlar tek bir koleksiyon üzerinde çalışır ve pipeline'dan daha az yetenek sunar. Map-reduce ise MongoDB 5.0'dan beri kullanımdan kaldırılmıştır; belgeler yerine aggregation pipeline kullanmayı önerir. SQL'deki WHERE, GROUP BY, HAVING, ORDER BY karşılıkları için MongoDB ve SQL karşılaştırma yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: $match'i sona koymak, İfadelerde dolar işaretini unutmak, Dahil etme ve hariç tutmayı karıştırmak, Büyük $sort'u limitsiz bırakmak
  • $match'i sona koymak: Filtre gecikirse önceki bütün aşamalar gereksiz belgeleri işler ve ilk aşamadaki indeks fırsatı kaçar.
  • İfadelerde dolar işaretini unutmak: $project içinde { toplam: "tutar" } metni atar; alanın değeri için "$tutar" gerekir.
  • Dahil etme ve hariç tutmayı karıştırmak: { ad: 1, sifre: 0 } geçersizdir; tek istisna _id: 0'dır.
  • Büyük $sort'u limitsiz bırakmak: Bellek sınırlarına takılır. İndeks ekleyin veya $limit kullanın.
  • Tekrarlanan değerlerle sıralayıp sayfalamak: Sıralama kararlı değildir; _id ekleyin.
  • Pipeline'ı dizi olmadan yazmak: Bu durumda allowDiskUse gibi seçenekleri veremezsiniz.

Sık Sorulan Sorular

Aggregation ile find arasında ne zaman hangisini seçmeliyim?

Yalnızca filtreleme, projeksiyon, sıralama ve sayfalama yapıyorsanız find() yeterlidir. Gruplama, hesaplanmış alanlar, birleştirme veya çok adımlı dönüşüm gerekiyorsa pipeline kullanın.

Aggregation verinizi değiştirir mi?

Hayır, $out veya $merge aşaması içermediği sürece pipeline koleksiyondaki belgeleri değiştirmez. Belgeleri pipeline ile güncellemek için güncelleme yöntemlerinin pipeline kabul eden biçimleri kullanılır.

Aşamaların sırası sonucu değiştirir mi?

Evet. Örneğin $limit ardından $sort, ilk n belgeyi alıp sıralar; $sort ardından $limit ise en büyük n belgeyi getirir. İyileştirici yalnızca sonucu değiştirmeyen dönüşümleri yapar.

Pipeline'daki her aşamanın çıktısını nasıl görürüm?

Pipeline'ı aşama aşama kurup her adımda aggregate()'i çalıştırarak ara sonuçları görebilirsiniz. Bu, uzun pipeline'lardaki hataları bulmanın en pratik yoludur.

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 · $group ve accumulator · $lookup ile join · $addFields ve $project · $out ve $merge · explain ve sorgu planı