MongoDB'de aggregation sonucunu bir koleksiyona yazmanın iki yolu vardır: $out hedef koleksiyonu sonuçla tamamen değiştirir, $merge ise sonucu mevcut koleksiyona belge belge birleştirir (ekler, günceller, korur ya da atar). İkisi de pipeline'ın son aşaması olmak zorundadır. Önceden hesaplanmış bu sonuç koleksiyonlarına on-demand materialized view denir; her sorguda yeniden hesaplanan standart view'lardan farklı olarak diskte saklanır.

Bu yazıda iki aşamanın söz dizimini ve seçeneklerini, aralarındaki farkları, $merge ile artımlı olarak tazelenen bir özet koleksiyonunun nasıl kurulacağını ve db.createView() ile oluşturulan standart view'ların ne zaman daha uygun olduğunu anlatıyoruz. Pipeline aşamalarına yeniyseniz aggregation pipeline yazısıyla başlayın.

$out: sonucu yeni bir koleksiyona yazmak

$out: sonucu yeni bir koleksiyona yazmak: Hedef sharded bir koleksiyon olamaz (girdi koleksiyonu sharded olabilir)..., Capped koleksiyona yazamaz., Transaction içinde, view...

$out iki biçimde yazılır. Dize verirseniz sonuç aynı veritabanındaki o koleksiyona gider; belge verirseniz hedef veritabanını da seçersiniz:

javascript
// Aynı veritabanına
{ $out: "aylikRapor" }

// Başka bir veritabanına
{ $out: { db: "raporlar", coll: "aylikRapor" } }

Replica set veya standalone sunucuda hedef veritabanı yoksa $out onu da oluşturur. MongoDB 7.0.3 ve 7.1'den beri timeseries seçeneğiyle time series koleksiyonuna da yazabilir; bu durumda timeField zorunludur. Time series koleksiyonlarının ayrıntıları time series yazısında anlatılıyor.

Mevcut koleksiyonun yerine geçme

Hedef koleksiyon zaten varsa $out onu aggregation tamamlandığında atomik olarak değiştirir. Belgelerdeki adımlar şöyledir: önce geçici bir koleksiyon oluşturulur, mevcut koleksiyonun indeks tanımları buraya kopyalanır, belgeler geçici koleksiyona yazılır ve son olarak renameCollection komutu dropTarget: true ile çalışarak geçici koleksiyonu hedefin yerine koyar.

Bu tasarımın pratik sonuçları önemlidir. Yeni bir koleksiyon aggregation bitene kadar görünmez; aggregation başarısız olursa koleksiyon hiç oluşmaz. Mevcut bir koleksiyon üzerine yazarken hata çıkarsa eski koleksiyona dokunulmaz. İndeks tanımları korunduğu için raporu okuyan sorgularınız aynı indekslerle çalışmaya devam eder. Öte yandan sonuçlar bir unique indeksi (hedefin _id indeksi dahil) ihlal ederse pipeline tamamlanamaz.

$out kısıtları

  • Hedef sharded bir koleksiyon olamaz (girdi koleksiyonu sharded olabilir). Sharded hedef için $merge kullanılır.
  • Capped koleksiyona yazamaz.
  • Transaction içinde, view tanımında, $lookup, $facet ve $unionWith alt pipeline'larında kullanılamaz.
  • Hedef koleksiyonda schema validation varsa ve validationAction değeri error ise, geçersiz bir belge işlemi hata ile durdurur; mevcut koleksiyon değişmez.
  • --oplog seçeneğiyle çalışan bir mongodump sürerken bir istemci $out içeren aggregation çalıştırırsa döküm başarısız olur.

$merge: sonucu mevcut koleksiyonla birleştirmek

$merge çok daha esnektir. Hedef koleksiyon yoksa onu oluşturur; varsa her sonuç belgesini, belirlediğiniz tanımlayıcı alanlara göre mevcut belgelerle karşılaştırır. Tam söz dizimi şöyledir:

javascript
{ $merge: {
     into: "hedef",                 // veya { db: "vt", coll: "hedef" }
     on: "_id",                     // isteğe bağlı; tek alan veya alan dizisi
     let: { },                      // isteğe bağlı; whenMatched pipeline'ı için değişkenler
     whenMatched: "merge",          // replace | keepExisting | merge | fail | pipeline
     whenNotMatched: "insert"       // insert | discard | fail
} }

Bütün varsayılanları kullanıyor ve aynı veritabanına yazıyorsanız kısa biçim yeterlidir: { $merge: "hedef" }. Varsayılanlar whenMatched: "merge" ve whenNotMatched: "insert" değerleridir.

whenMatched seçenekleri

DeğerEşleşen belge bulunduğunda
merge (varsayılan)Alanları birleştirir: yeni alanlar eklenir, ortak alanlar sonuçtaki değerle değişir
replaceMevcut belgeyi sonuç belgesiyle tamamen değiştirir
keepExistingMevcut belgeyi olduğu gibi bırakır
failİşlemi durdurur; o ana kadar yazılanlar geri alınmaz
pipelineMevcut belgeyi özel bir güncelleme pipeline'ı ile değiştirir

Belgelerdeki örneğe benzer şekilde düşünürsek: hedefte { _id: 1, a: 1, b: 1 }, sonuçta { _id: 1, b: 5, z: 1 } varsa merge sonucu { _id: 1, a: 1, b: 5, z: 1 } olur. replace seçseydiniz a alanı kaybolurdu.

Pipeline biçiminde yalnızca $addFields/$set, $project/$unset ve $replaceRoot/$replaceWith aşamaları kullanılabilir. Pipeline içinde $alan hedef koleksiyondaki mevcut belgeyi, $$new.alan ise yeni sonuç belgesini gösterir. $$new değişkeni yalnızca let yazılmadığında kullanılabilir; let belirtmezseniz varsayılanı { new: "$$ROOT" } olur. Pipeline, on alanlarının değerini değiştiremez.

javascript
db.satislar.aggregate( [
   { $group: { _id: "$urunId", adet: { $sum: "$adet" } } },
   { $merge: {
        into: "urunToplamlari",
        whenMatched: [ { $set: { adet: { $add: [ "$adet", "$$new.adet" ] } } } ],
        whenNotMatched: "insert"
   } }
] )

Bu sorgu her ürün için satış adetlerini toplar. Ürün hedefte zaten varsa yeni adedi mevcut adede ekler, yoksa yeni belge olarak yazar. Böyle bir pipeline'ı aynı veri üzerinde iki kez çalıştırırsanız adetler iki kez eklenir; bu yüzden toplanan veriyi bir tarih aralığıyla sınırlamak gerekir.

on alanı ve unique indeks şartı

on, bir sonuç belgesinin hedefteki hangi belgeyle eşleştiğine karar veren alan veya alanlardır. Kurallar şöyledir:

  • $merge, on alanlarına karşılık gelen ve yalnızca bu alanları anahtar olarak içeren bir unique indeks ister. Hedef koleksiyon zaten varsa bu indeks de önceden var olmalıdır.
  • İndeks sparse olabilir ama partial olamaz; aggregation ile aynı collation'a sahip olmalıdır.
  • Hedef koleksiyon yoksa on yalnızca _id olabilir ve _id indeksi otomatik oluşur. Başka bir alan kullanmak istiyorsanız koleksiyonu önce o alanlar üzerinde unique indeks oluşturarak yaratın.
  • Sonuç belgelerinde on alanları bulunmalıdır (alan _id ise ve yoksa MongoDB onu ekler). Alanlar dizi değeri taşıyamaz.
  • MongoDB 8.0 ve öncesinde on alanları eksik veya null olamaz; 8.1'den beri destekleyen indeks sparse değilse eksik veya null olabilir.
  • Hedef sharded bir koleksiyonsa varsayılan on, shard key alanları ile _id alanıdır; kendi on değerinizi verirseniz bütün shard key alanlarını içermelidir.
javascript
// Hedef henüz yoksa önce unique indeksi oluşturun
db.gunlukOzet.createIndex( { gun: 1 }, { unique: true } )

db.siparisler.aggregate( [
   { $group: {
        _id: { $dateToString: { format: "%Y-%m-%d", date: "$tarih" } },
        ciro: { $sum: "$tutar" }
   } },
   { $project: { _id: 0, gun: "$_id", ciro: 1 } },
   { $merge: { into: "gunlukOzet", on: "gun", whenMatched: "replace", whenNotMatched: "insert" } }
] )

$project aşamasında _id: 0 yazmamızın nedeni şudur: on alanı _id değilken sonuçta _id bulunursa ve hedefteki belgenin _id değeri farklıysa, replace veya merge mevcut belgenin _id değerini değiştirmeye çalışır ve $merge hata verir. Belgeler bu durumda _id alanını önceki bir aşamada çıkarmayı önerir. Sonuç belgesinde _id yoksa $merge yeni eklenen belge için onu kendisi üretir.

Hata ve geri alma davranışı

$out ile en büyük fark burada ortaya çıkar. $merge hedefi ilk belgeyi yazdığı anda oluşturur ve koleksiyon hemen görünür olur. Aggregation yarıda hata verirse o ana kadar yazılan belgeler geri alınmaz. Schema validation hatasında ilk geçersiz belge hatayı fırlatır, geçerli belgeler yine de yazılır. Bu yüzden $merge içeren işleri tekrar çalıştırıldığında doğru sonuç verecek şekilde (örneğin replace ile ve sınırlı tarih aralığıyla) tasarlamak gerekir.

Aynı koleksiyona yazmak

$merge, aggregate edilen koleksiyonun kendisine de yazabilir. Ancak belgeler bunun belgelerin birden fazla kez güncellenmesine, hatta sonsuz döngüye yol açabileceği konusunda uyarır: güncelleme belgenin diskteki fiziksel konumunu değiştirirse $merge onu yeni bir belge sanabilir. Bu duruma Halloween problemi denir. Mümkünse ayrı bir hedef koleksiyon kullanın.

$out ve $merge karşılaştırması

$out ve $merge karşılaştırması: Hedef veritabanı, Hedef yoksa, Hedef varsa, Sharded hedef
Özellik$out$merge
Hedef veritabanıAynı veya farklıAynı veya farklı
Hedef yoksaOluştururOluşturur
Hedef varsaTamamen değiştirirBelge bazında ekler, birleştirir, değiştirir veya korur
Sharded hedefHayırEvet
Time series hedefEvet (7.0.3 ve 7.1 sonrası)Hayır
Hata olursaMevcut koleksiyon değişmezYazılmış belgeler geri alınmaz
Transaction ve view içindeKullanılamazKullanılamaz
SQL karşılığıSELECT INTO, INSERT INTO SELECTMERGE, INSERT INTO SELECT

Belgeler önemli bir ayrıntıyı da vurgular: $merge ile replace kullanarak koleksiyonun bütün belgelerini değiştirebilirsiniz, ama bu yalnızca sonuçlar mevcut her belge için bir eşleşme içeriyorsa olur. Sonuçtan bağımsız olarak koleksiyonu baştan kurmak istiyorsanız doğru araç $out aşamasıdır.

Mini örnek: artımlı tazelenen materialized view

Materialized view, diskte saklanan ve okunurken yeniden hesaplanmayan önceden hesaplanmış bir aggregation sonucudur. Belgelerdeki yöntem, tazeleme işini bir mongosh fonksiyonuna koyup yalnızca değişen dönemi yeniden hesaplamaktır. Aşağıdaki fonksiyon, verilen günden itibaren günlük ciroyu yeniden hesaplar:

javascript
function gunlukOzetiTazele(baslangic) {
   db.siparisler.aggregate( [
      { $match: { tarih: { $gte: baslangic } } },
      { $group: {
           _id: { $dateToString: { format: "%Y-%m-%d", date: "$tarih" } },
           siparisSayisi: { $sum: 1 },
           ciro: { $sum: "$tutar" }
      } },
      { $merge: { into: "gunlukCiro", whenMatched: "replace", whenNotMatched: "insert" } }
   ] )
}

// İlk kurulum: geçmişin tamamı
gunlukOzetiTazele( new Date("2026-01-01") )

// Sonraki çalıştırmalar: yalnızca son günler
gunlukOzetiTazele( new Date("2026-11-06") )

Grup anahtarı günün tarihi olduğu için _id her gün için benzersizdir ve on yazmamıza gerek kalmaz; hedef ilk çalıştırmada otomatik oluşur. whenMatched: "replace" sayesinde aynı gün tekrar hesaplandığında eski satır yenisiyle değişir, yani fonksiyonu aynı aralıkla birden fazla kez çalıştırmak sonucu bozmaz. Raporu okuyan uygulama artık yalnızca db.gunlukCiro.find() yapar ve büyük siparisler koleksiyonunu taramaz.

Standart view: kaydedilmiş sorgu

Standart view: kaydedilmiş sorgu: View, kaynak koleksiyonla aynı veritabanında oluşturulmalıdır ve..., View pipeline'ı, iç içe pipeline'lar dahil, $out veya $merge içeremez...

Standart view, başka bir koleksiyon veya view üzerinde tanımlanmış bir aggregation pipeline'ıdır. İçeriği diske yazılmaz, her sorguda yeniden hesaplanır ve salt okunurdur; view'a yazma işlemi hata döndürür.

javascript
db.createView(
   "aktifMusteriler",
   "musteriler",
   [ { $match: { aktif: true } }, { $unset: [ "tcKimlik", "telefon" ] } ]
)

db.aktifMusteriler.find( { sehir: "İzmir" } )

Aynı view'ı db.createCollection("aktifMusteriler", { viewOn: "musteriler", pipeline: [ ... ] }) biçiminde de oluşturabilirsiniz. Belgelerdeki kullanım örneklerinden biri tam olarak budur: kişisel verileri dışarıda bırakan bir view tanımlamak. Erişim denetimi açıksa view'ı sorgulamak için yalnızca view üzerinde find yetkisi gerekir; kaynak koleksiyonda find yetkisi gerekmez.

View kuralları

  • View, kaynak koleksiyonla aynı veritabanında oluşturulmalıdır ve oluşturulduktan sonra yeniden adlandırılamaz.
  • View pipeline'ı, iç içe pipeline'lar dahil, $out veya $merge içeremez.
  • Standart view kaynak koleksiyonun indekslerini kullanır; view üzerinde doğrudan genel indeks oluşturamazsınız.
  • Collation belirtilmezse view kaynak koleksiyonun collation ayarını devralmaz, basit ikili karşılaştırma kullanır.
  • find() ile view sorgularken $, $elemMatch, $slice projeksiyon operatörleri ve $meta desteklenmez; $text ve map-reduce da kullanılamaz.
  • View'ı değiştirmek için ya silip yeniden oluşturursunuz ya da collMod komutunu viewOn ve pipeline ile çalıştırırsınız.
  • View pipeline'ı bloklayan sıralama ve gruplama için 100 megabaytlık bellek sınırına tabidir.

Standart view mı, materialized view mı?

ÖzellikStandart viewMaterialized view
SaklamaDiske yazılmaz, okunurken hesaplanırDiske yazılır, diskten okunur
TazelikHer zaman güncelSon tazeleme anı kadar güncel
İndeksKaynak koleksiyonun indeksleriKendi indeksleri oluşturulabilir
Okuma performansıPipeline karmaşıklığına bağlıDaha iyi; fark veri ve pipeline büyüdükçe artar
Oluşturmadb.createView()$merge veya $out içeren aggregation

Kısaca: veri her an güncel olmalıysa ve sorgu ucuzsa standart view, sorgu pahalıysa ve birkaç dakikalık gecikme kabul edilebiliyorsa materialized view seçin. MongoDB 9.0 ile $convert bir nesneyi binData türüne dönüştürebiliyor; bu dönüşümü kullanan view'lar eski sürüme dönüldüğünde hata verir. Ayrıntı için MongoDB 9.0 yenilikleri yazısına bakın.

Sık yapılan hatalar

  • $out ile canlı koleksiyonu ezmek: hedef adını yanlış yazarsanız veri kaybedersiniz.
  • on alanı için unique indeks oluşturmamak: mevcut hedefte $merge bu indeks olmadan çalışmaz.
  • on alanı _id değilken sonuçta _id bırakmak: eşleşen belgede _id değişmeye çalışır ve işlem hata verir.
  • $merge hatasında geri alma beklemek: yazılmış belgeler kalır; işi yeniden çalıştırılabilir tasarlayın.
  • View'a yazmaya çalışmak: standart view salt okunurdur.
  • Pahalı pipeline'ı standart view yapmak: her okuma pipeline'ı baştan çalıştırır; bu durumda materialized view daha uygundur.

Sık Sorulan Sorular

$out ile yazılan koleksiyonun indeksleri kaybolur mu?

Hayır. Belgelere göre $out mevcut koleksiyonun indeks tanımlarını kopyalar ve yeni koleksiyonda yeniden oluşturur. Hedef ilk kez oluşuyorsa elbette yalnızca _id indeksi olur; gereken diğer indeksleri bir kez oluşturmanız yeterlidir.

$merge ile çalışan işi secondary üyede çalıştırabilir miyim?

MongoDB 5.0'dan beri, kümedeki bütün üyelerin feature compatibility version değeri 5.0 veya üzerindeyse ve read preference secondary okumaya izin veriyorsa $merge ve $out içeren pipeline'lar secondary üyede çalışabilir; yazma işlemleri yine primary üyeye gider. Sürücünüzün bunu destekleyip desteklemediğini sürücü belgelerinden kontrol edin. Read preference konusu write concern ve read concern yazısında anlatılıyor.

Materialized view kendiliğinden güncellenir mi?

Hayır; bu yazıda anlatılan view'lar "on-demand" türdendir, yani tazeleme pipeline'ını siz çalıştırırsınız. Bunu uygulamanızdaki bir zamanlanmış görevle veya sunucunuzdaki bir zamanlayıcıyla düzenli hale getirebilirsiniz.

$lookup sonucunu bir view içinde kullanabilir miyim?

Evet, view pipeline'ı $lookup içerebilir. Birden fazla view'ı birleştiriyorsanız bunların collation ayarları aynı olmalıdır.

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 · Aggregation pipeline · $group ve accumulator · $lookup ile join · Unique ve partial index · mongodump ve mongorestore