MongoDB aggregation pipeline'da belgelerin şeklini değiştirmek için dört aşama kullanılır: $addFields ve onun takma adı $set mevcut alanları koruyup yeni alan ekler; $project hangi alanların bir sonraki aşamaya geçeceğini belirler ve hesaplanmış alan üretebilir; $unset ise belirtilen alanları çıkarır. Kısacası alan eklemek için $set, çıktıyı daraltmak için $project, birkaç alanı atmak için $unset en okunur seçimdir.

Bu yazıda dört aşamanın kurallarını, birbirleriyle ilişkilerini, ifade (expression) operatörleriyle hesaplama yapmayı, $$REMOVE değişkenini ve özellikle $project aşamasında sık karşılaşılan hata mesajlarının nedenlerini anlatıyoruz. Pipeline'ın genel yapısı için aggregation pipeline yazısına göz atabilirsiniz.

Örnek veri

javascript
db.kitaplar.insertMany( [
   { _id: 1, baslik: "Deniz Feneri", fiyat: 120, kdv: 0.10,
     yazar: { ad: "Selin", soyad: "Kaya" }, etiketler: [ "roman", "deniz" ] },
   { _id: 2, baslik: "Kuzey Yolu", fiyat: 80, kdv: 0.10,
     yazar: { ad: "Can", soyad: "Demir" } }
] )

İkinci belgede etiketler alanı yok. Bu ayrıntı, aşağıda $size gibi operatörlerin eksik alanlarda nasıl davrandığını göstermek için önemli olacak.

$addFields ve $set: mevcut belgeye alan eklemek

$addFields ve $set: mevcut belgeye alan eklemek: Yeni alanın adı mevcut bir alanla aynıysa (_id dahil), mevcut değer ifadenin..., Bir pipeline'da birden fazla $addFields veya $set...

$addFields, girdi belgesindeki bütün alanları korur ve verdiğiniz yeni alanları ekler. Belgelere göre bu aşama, mevcut bütün alanları tek tek dahil edip yenilerini ekleyen bir $project ile eşdeğerdir. $set aşaması ise $addFields için bir takma addır; ikisi arasında davranış farkı yoktur. Günlük kullanımda daha kısa olduğu için $set tercih edilebilir.

javascript
db.kitaplar.aggregate( [
   { $set: {
        kdvliFiyat: { $multiply: [ "$fiyat", { $add: [ 1, "$kdv" ] } ] },
        tamAd: { $concat: [ "$yazar.ad", " ", "$yazar.soyad" ] },
        "yazar.ulke": "TR"
   } }
] )

Bu sorgu her kitaba KDV dahil fiyatı ve yazarın tam adını ekler, ayrıca gömülü yazar belgesine nokta gösterimiyle ulke alanını yazar. Değerler bir aggregation ifadesi olabilir: "$fiyat" gibi dolar işaretiyle başlayan dizeler alan yoludur, "TR" gibi düz dizeler sabittir.

Dikkat edilecek kurallar

  • Yeni alanın adı mevcut bir alanla aynıysa (_id dahil), mevcut değer ifadenin sonucuyla üzerine yazılır. Bu, bir alanı yerinde güncellemenin de yoludur.
  • Bir pipeline'da birden fazla $addFields veya $set aşaması kullanabilirsiniz. Belgelerdeki örnek de film süresini önce bir aşamada saate çevirir, lisans ücretini ise ikinci bir $addFields aşamasında bu yeni alandan hesaplar. Ara sonuca dayanan hesapları bu şekilde ayrı aşamalara bölmek okunurluğu artırır.
  • Var olan bir diziye eleman eklemek için $concatArrays kullanılır; alan adı aynı tutulursa dizi genişletilmiş haliyle yeniden yazılır.
  • $$REMOVE değişkenine çözümlenen bir alan çıktıdan çıkarılır. Böylece $addFields ile koşullu alan silme yapılabilir.
  • İç içe nesne de yazabilirsiniz: { $addFields: { a: { b: { } } } } geçerlidir.
javascript
db.kitaplar.aggregate( [
   { $set: {
        etiketler: { $concatArrays: [ { $ifNull: [ "$etiketler", [ ] ] }, [ "stokta" ] ] },
        kdv: { $cond: { if: { $gte: [ "$fiyat", 100 ] }, then: "$kdv", else: "$$REMOVE" } }
   } }
] )

İlk ifade, etiketler alanı eksik olan belgelerde $ifNull sayesinde boş diziden başlar ve her kitaba stokta etiketini ekler. $ifNull eksik alanları ve tanımsız değerleri de null gibi ele alır. İkinci ifade, fiyatı 100'ün altındaki kitaplarda kdv alanını $$REMOVE ile çıktıdan siler. $cond her iki yazımda da üç bölümün (if, then, else) tamamını ister; birini atlarsanız hata alırsınız.

$set aşaması ile $set güncelleme operatörü aynı şey değil

Aynı adı taşısalar da aggregation'daki $set aşaması ile updateOne içinde kullanılan $set güncelleme operatörü farklı belgelenmiş iki ayrı araçtır. Güncelleme operatörü bir alanı sabit değere ayarlar (updateOne ve updateMany yazısında anlatılıyor). Aggregation aşaması ise ifade değerlendirir: başka alanlardan hesap yapabilir, koşul kurabilir.

İki dünya, güncelleme komutuna dizi biçiminde bir pipeline verdiğinizde birleşir. Belgelere göre güncelleme pipeline'larında yalnızca şu aşamalar desteklenir: $addFields, $set, $project, $unset, $replaceRoot ve $replaceWith.

javascript
db.kitaplar.updateMany(
   { },
   [ { $set: { kdvliFiyat: { $multiply: [ "$fiyat", { $add: [ 1, "$kdv" ] } ] } } } ]
)

Köşeli parantezler bu güncellemeyi pipeline güncellemesi yapar; bu sayede kdvliFiyat her belgenin kendi fiyat ve kdv değerinden hesaplanır. Köşeli parantez yoksa ikinci argüman sıradan bir güncelleme belgesi olarak ele alınır; başka alanlardan değer hesaplayan ifadeler ancak pipeline biçiminde kullanılabilir.

$project: çıktının şeklini belirlemek

$project: çıktının şeklini belirlemek: _id varsayılan olarak her zaman çıktıdadır; istemiyorsanız açıkça _id: 0..., yazarSoyadi, $size, argümanı eksikse veya dizi değilse hata...

$project, belgeyi istediğiniz alanlara indirger ve aynı anda yeni alanlar hesaplayabilir. Kabul ettiği belirtim biçimleri şunlardır:

BiçimAnlamı
alan: 1 veya trueAlanı çıktıya dahil et (sıfır olmayan tamsayılar da true sayılır)
_id: 0 veya false_id alanını çıktıdan çıkar
alan: ifadeYeni alan ekle veya mevcut alanın değerini yeniden belirle
alan: 0 veya falseAlanı çıkar; bu durumda başka bir belirtim biçimi kullanılamaz
javascript
db.kitaplar.aggregate( [
   { $project: {
        _id: 0,
        baslik: 1,
        yazarSoyadi: "$yazar.soyad",
        etiketSayisi: { $size: { $ifNull: [ "$etiketler", [ ] ] } },
        indirimde: { $literal: true }
   } }
] )

Bu sorgu yalnızca başlığı, yazarın soyadını, etiket sayısını ve sabit indirimde alanını döndürür. Birkaç ayrıntı dikkat ister:

  • _id varsayılan olarak her zaman çıktıdadır; istemiyorsanız açıkça _id: 0 yazmalısınız.
  • yazarSoyadi: "$yazar.soyad" bir alanı yeni adla göstermenin, yani yeniden adlandırmanın yoludur.
  • $size, argümanı eksikse veya dizi değilse hata verir. İkinci kitapta etiketler olmadığı için $ifNull ile boş dizi vermezsek sorgu hata ile durur.
  • Sayısal veya mantıksal bir sabiti doğrudan yazarsanız $project onu dahil etme veya çıkarma bayrağı sanır. indirimde: true bir sabit değil, alanı dahil etme isteğidir; sabit için $literal gerekir.
  • Belgede olmayan bir alanı dahil etmek isterseniz $project bunu sessizce yok sayar ve alanı eklemez.

Dahil etme ile çıkarmayı karıştırmak

$project aşamasındaki en yaygın hata, aynı aşamada hem alan dahil etmek hem de _id dışında bir alanı çıkarmaktır. Belgelere göre _id dışındaki bir alanı 0 ile çıkardığınızda başka hiçbir belirtim biçimi kullanamazsınız: alan dahil edemez, değer değiştiremez, yeni alan ekleyemezsiniz. Bu kural $$REMOVE ile yapılan koşullu çıkarma için geçerli değildir.

javascript
// Geçersiz: dahil etme (baslik: 1) ile çıkarma (fiyat: 0) bir arada
db.kitaplar.aggregate( [ { $project: { baslik: 1, fiyat: 0 } } ] )

// Geçerli: yalnızca çıkarma
db.kitaplar.aggregate( [ { $project: { fiyat: 0, kdv: 0 } } ] )

Gömülü alanlar ve yol çakışması

Gömülü bir alanı nokta gösterimiyle ("yazar.ad": 1) veya iç içe belgeyle (yazar: { ad: 1 }) belirtebilirsiniz. İç içe biçimi seçtiyseniz içeride ayrıca nokta kullanamazsınız; yazar: { "bilgi.ad": 1 } geçersizdir. Aynı projeksiyonda hem bir gömülü belgeyi hem de onun içindeki bir alanı belirtmek de yasaktır: { yazar: 1, "yazar.ad": 1 } bir Path collision hatası üretir ve alanların sırası sonucu değiştirmez.

Diğer kurallar

  • $project aşamasına boş belge ({ }) verirseniz MongoDB hata döndürür.
  • $project içinde dizi indeksi kullanamazsınız; "etiketler.0": 1 ile ilk elemanı seçemezsiniz. Bunun için $arrayElemAt gibi bir ifade kullanın.
  • Köşeli parantezle doğrudan yeni dizi alanı oluşturabilirsiniz: { $project: { ikili: [ "$baslik", "$fiyat" ] } }. Belgede olmayan alanlara başvuran elemanların yerine null konur.

$project aşamasını nereye koymalı?

Belgeler, $project aşamasının genellikle pipeline'ın son aşaması olmasını, yani istemciye dönecek alanları belirlemek için kullanılmasını önerir. Pipeline'ın başında veya ortasında alan sayısını azaltmak için $project eklemek büyük olasılıkla performansı artırmaz, çünkü veritabanı bu iyileştirmeyi kendisi otomatik olarak yapar. Yani $project aşamasını bir hız aracı olarak değil, çıktının sözleşmesini tanımlayan bir araç olarak düşünün.

$unset aşaması: alan çıkarmanın kısa yolu

$unset, belirttiğiniz alanları belgeden çıkarır. Tek alan için dize, birden fazla alan için dizi alır ve gömülü alanlar için nokta gösterimini destekler. Belgelere göre $unset, alanları 0 ile çıkaran bir $project aşamasının takma adıdır.

javascript
db.kitaplar.aggregate( [
   { $match: { _id: 2 } },
   { $unset: [ "kdv", "yazar.soyad" ] }
] )
// çıktı: { _id: 2, baslik: "Kuzey Yolu", fiyat: 80, yazar: { ad: "Can" } }

Çıktıda kdv alanı ve gömülü yazar belgesindeki soyad alanı yok; diğer her şey olduğu gibi kaldı. Belge dizisi içindeki bir alanı da aynı yolla çıkarabilirsiniz: "kopyalar.depo" gibi bir yol, dizideki her gömülü belgeden depo alanını siler.

Aggregation'daki $unset aşaması da $set gibi aynı adlı güncelleme operatöründen farklıdır: güncelleme operatörü veritabanındaki belgeden alanı kalıcı olarak siler, aggregation aşaması ise yalnızca pipeline çıktısını değiştirir ve veriye dokunmaz.

Hangisini ne zaman kullanmalı?

Hangisini ne zaman kullanmalı? $addFields, $set, $project, $unset
AşamaDiğer alanlarHesaplamaTipik kullanım
$addFieldsKorunurEvetMevcut belgeye hesaplanmış alan eklemek
$setKorunurEvet$addFields ile aynı; kısa yazım
$projectYalnızca belirtilenler kalırEvetPipeline sonunda çıktı şeklini belirlemek
$unsetBelirtilenler dışında korunurHayırHassas veya gereksiz alanları atmak

Belgeler, alan çıkarmak için hem $addFields ($$REMOVE ile) hem de $project kullanılabileceğini ve en iyi seçimin orijinal belgenin ne kadarını korumak istediğinize bağlı olduğunu söyler. Belgenin çoğunu tutup birkaç alanı atacaksanız $unset, birkaç alanı tutup gerisini atacaksanız $project daha kısa ve okunur olur.

MongoDB 9.0 ile gelen bellek ölçümü

MongoDB 9.0 sürüm notlarına göre klasik sorgu motoru, explain çıktısında executionStats ve üzeri ayrıntı düzeyinde expressionEvaluationPeakMemoryBytes alanını raporluyor. Bu alanın raporlandığı aşamalar arasında $project, $addFields, $set ve $unset da var. Aynı sürümde tek bir ifade değerlendirmesinin kullanabileceği bellek internalQueryMaxSingleExpressionMemoryUsageBytes parametresiyle (varsayılan 100 megabayt) ve tek bir sorgu işleminin toplam belleği ayrı bir parametreyle sınırlanıyor. Çok büyük dizileri birleştiren veya dizeleri art arda ekleyen ifadeler yazıyorsanız bu sınırları MongoDB 9.0 yenilikleri yazısında inceleyin; planı ölçmek için explain() rehberine bakın.

Sık yapılan hatalar

  • Dahil etme ve çıkarmayı karıştırmak: { baslik: 1, fiyat: 0 } geçersizdir; yalnızca _id: 0 dahil etmeyle birlikte kullanılabilir.
  • Sabit yerine bayrak yazmak: $project içinde alan: 1 sabit 1 değil, dahil etme isteğidir. Sabit için $literal kullanın.
  • Eksik dizide $size: alan eksikse hata alırsınız; $ifNull ile varsayılan dizi verin.
  • $concat ile eksik alan: argümanlardan biri null ise veya eksik bir alana başvuruyorsa $concat sonucu null olur. Gerekirse her parçayı $ifNull ile sarın.
  • Pipeline güncellemesinde köşeli parantezi unutmak: hesaplanmış güncelleme için ikinci argüman dizi olmalıdır.
  • Aynı adlı alanı farkında olmadan ezmek: $set mevcut alanın üzerine yazar, uyarı vermez.

Sık Sorulan Sorular

$addFields ile $set arasında performans farkı var mı?

Belgelere göre $set, $addFields aşamasının takma adıdır; aynı aşamanın iki adıdır. Hangisini seçtiğiniz okunurluk tercihidir.

$project ile alanları erken atmak sorguyu hızlandırır mı?

Belgeler bunun büyük olasılıkla performansı iyileştirmeyeceğini, çünkü veritabanının gereksiz alanları zaten otomatik olarak ayıkladığını belirtir. $project aşamasını çıktıyı tanımlamak için pipeline sonunda kullanın; gerçek hız kazancı genellikle erken $match ve doğru indeksten gelir.

Bir alanı yeniden adlandırmanın en kolay yolu nedir?

Aggregation içinde yeni alanı eski alanın yoluyla tanımlayıp eskisini çıkarabilirsiniz: { $set: { yeniAd: "$eskiAd" } } ardından { $unset: "eskiAd" }. Veritabanındaki belgeleri kalıcı olarak değiştirmek istiyorsanız güncelleme tarafındaki $rename operatörünü kullanın; o konu güncelleme yazısının kapsamında.

Hesaplanmış alanları bir koleksiyona kaydedebilir miyim?

Evet. Pipeline sonunda $merge veya $out kullanarak sonuçları yazabilir ya da pipeline güncellemesiyle alanı belgelerin kendisine ekleyebilirsiniz. Ayrıntılar için $out ve $merge yazısına bakın.

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 · updateOne ve updateMany · $out ve $merge