Aggregation Pipeline 2: $setWindowFields, $graphLookup, $bucket, $merge ile Materialized View ve Bellek Limitleri
MongoDB'de window function'ların karşılığı $setWindowFields; ama eksik günleri $densify ile eklemeden yapılan hareketli ortalama, takvim günlerine değil var olan document'lere göre hesaplanır. Gerçek çıktılarla SQL Temelleri'ndeki running total, top-N ve 7 günlük ortalamayla birebir aynı sonuçlar, $graphLookup ile kategori ağacı, $bucketAuto, $merge ile artımlı yenilenen materialized view, 9.0'ın sorgu başına bellek sınırı ve $function.
Sürüm notu: Bu yazıdaki örnekler MongoDB 9.0.8 Community Server ve mongosh 2.6 ile çalıştırılmıştır. Script'ler MongoDB 8.0.38'de de aynı sonucu verdi; sorgu başına bellek sınırı yalnız 9.0'da var, farkı ilgili bölümde.
Önce Tahmin Et
SQL Temelleri serisinin window function'lar yazısında Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) Aralık 2025 siparişleri için 7 günlük hareketli ortalama hesaplanmıştı ve 21 Aralık için doğru değer, siparişsiz günler 0 sayıldığında 1.00 çıkmıştı. Aynı hesabı MongoDB'de iki pencereyle yapalım: son 7 document ve son 7 gün.
// 08 - Önce Tahmin Et: günlük siparişlerin 7 günlük hareketli ortalaması, Aralık 2025
// SQL Temelleri 14: 2025-12-21 için avg_7_days 1.00 (siparişsiz günler 0 sayılır)
db.orders.aggregate([
{ $match: { orderedAt: { $gte: ISODate("2025-12-01"), $lt: ISODate("2026-01-01") } } },
{ $group: { _id: { $dateTrunc: { date: "$orderedAt", unit: "day" } }, orders: { $sum: 1 } } },
{ $setWindowFields: {
sortBy: { _id: 1 },
output: {
byDocuments: { $avg: "$orders", window: { documents: [-6, 0] } },
byRange: { $avg: "$orders", window: { range: [-6, 0], unit: "day" } }
}
} },
{ $match: { _id: { $gte: ISODate("2025-12-20"), $lt: ISODate("2025-12-24") } } }
]).forEach(d => print(d._id.toISOString().slice(0, 10), "orders", d.orders,
" documents:", d.byDocuments.toFixed(2), " range:", d.byRange.toFixed(2)));
range penceresi "son 7 gün" demek; o zaman hangisi 1.00 verir?
2025-12-21 orders 2 documents: 1.43 range: 1.40
2025-12-22 orders 4 documents: 1.86 range: 2.00
2025-12-23 orders 1 documents: 1.71 range: 1.83
Hiçbiri. documents penceresi son 7 document'e bakıyor; SQL'deki ROWS BETWEEN 6 PRECEDING sonucuyla (1.43) aynı ve o yazıda gördüğümüz gibi siparişsiz günler yokmuş gibi davranıyor. range penceresi doğru günleri seçiyor ama $avg bölmeyi pencereye düşen document sayısıyla yapıyor: 15–21 Aralık arasında siparişi olan beş gün var, 7 siparişi 5'e bölüyor ve 1.40 buluyor. 20 Aralık ise çıktıda hiç yok, çünkü o gün sipariş yok ve $group olmayan bir gün için document üretmez. Sorun pencerede değil veride: takvim günleri üzerinden ortalama istiyorsan önce eksik günleri eklemen gerekiyor. Bu yazıda pipeline'ın ileri stage'lerini aynı yaklaşımla, sonuçları SQL Temelleri'ndeki sayılarla karşılaştırarak geçiyoruz. Script'ler uygulama kullanıcısıyla çalışıyor; yalnız bellek sınırı denemesi admin istiyor.
$setWindowFields: Running Total ve Grup İçinde Sıralama
$setWindowFields SQL'deki OVER (PARTITION BY ... ORDER BY ...) yapısının karşılığı: document'leri gruplamadan, her document'e bir pencere üzerinden hesaplanmış alanlar ekliyor. partitionBy PARTITION BY'a, sortBy ORDER BY'a, window çerçeveye karşılık geliyor.
// Aylık ciro ve yıl başından beri toplam (SQL Temelleri 14: SUM(...) OVER (ORDER BY month ROWS UNBOUNDED PRECEDING))
db.orders.aggregate([
{ $match: { status: { $ne: "Cancelled" }, orderedAt: { $gte: ISODate("2025-01-01"), $lt: ISODate("2026-01-01") } } },
{ $group: { _id: { $dateTrunc: { date: "$orderedAt", unit: "month" } }, revenue: { $sum: "$total" } } },
{ $setWindowFields: {
sortBy: { _id: 1 },
output: { ytd: { $sum: "$revenue", window: { documents: ["unbounded", "current"] } } }
} }
]).forEach(m => print(m._id.toISOString().slice(0, 7), m.revenue.toString().padStart(9), m.ytd.toString().padStart(10)));
// Grup başına top-N (SQL Temelleri 13): her kategorinin adet olarak en çok satan kitabı
db.orders.aggregate([
{ $match: { status: { $ne: "Cancelled" } } },
{ $unwind: "$items" },
{ $group: { _id: "$items.bookId", units: { $sum: "$items.quantity" } } },
{ $lookup: { from: "books", localField: "_id", foreignField: "_id", as: "book" } },
{ $set: { category: { $first: "$book.category.name" }, title: { $first: "$book.title" } } },
{ $setWindowFields: { partitionBy: "$category", sortBy: { units: -1 }, output: { rank: { $rank: {} } } } },
{ $match: { rank: 1, category: { $in: ["Science Fiction", "Programming", "Dystopian"] } } },
{ $sort: { category: 1 } }
]).forEach(b => print(b.category.padEnd(16), b.title.padEnd(26), "units", b.units));
2025-01 8959.20 8959.20
2025-02 13727.50 22686.70
2025-03 20367.60 43054.30
2025-04 18488.20 61542.50
2025-05 13679.60 75222.10
2025-06 15414.30 90636.40
2025-07 12866.70 103503.10
2025-08 12681.50 116184.60
2025-09 33349.23 149533.83
2025-10 27489.90 177023.73
2025-11 20325.80 197349.53
2025-12 41866.20 239215.73
Dystopian Nineteen Eighty-Four units 30
Programming The Pragmatic Programmer units 16
Science Fiction Dune units 32
On iki ayın hem cirosu hem yıl başından beri toplamı SQL Temelleri'ndeki tabloyla kuruşu kuruşuna aynı. documents: ["unbounded", "current"] SQL'deki ROWS UNBOUNDED PRECEDING; SQL Server'da ROWS yazmayınca varsayılan RANGE çerçevesinin yavaş çalıştığını o yazıda görmüştük, burada çerçeveyi her zaman açıkça yazıyorsun. İkinci sorgu grup başına top-N: $rank kategori içinde satış sırasını veriyor, rank: 1 her kategorinin en çok satanını bırakıyor. Eşitlik varsa $rank iki kitaba da 1 verir; tek kayıt istiyorsan $documentNumber (SQL'deki ROW_NUMBER) kullan.
$densify ve $fill: Eksik Günleri Eklemek
SQL'de eksik günler için bir takvim tablosu (o yazıda GENERATE_SERIES) üretip LEFT JOIN yapmıştık. MongoDB'de bunun karşılığı iki stage: $densify bir alanın verilen adımla eksik değerleri için document ekliyor, $fill bu yeni document'lerdeki boş alanları dolduruyor.
// 08 - $densify + $fill: siparişsiz günleri ekle, sonra takvim günleri üzerinden ortalama al
db.orders.aggregate([
{ $match: { orderedAt: { $gte: ISODate("2025-12-01"), $lt: ISODate("2026-01-01") } } },
{ $group: { _id: { $dateTrunc: { date: "$orderedAt", unit: "day" } }, orders: { $sum: 1 } } },
{ $densify: { field: "_id", range: { step: 1, unit: "day", bounds: [ISODate("2025-12-01"), ISODate("2026-01-01")] } } },
{ $fill: { output: { orders: { value: 0 } } } },
{ $setWindowFields: {
sortBy: { _id: 1 },
output: { avg7: { $avg: "$orders", window: { range: [-6, 0], unit: "day" } } }
} },
{ $match: { _id: { $gte: ISODate("2025-12-20"), $lt: ISODate("2025-12-24") } } }
]).forEach(d => print(d._id.toISOString().slice(0, 10), "orders", d.orders, " avg7:", d.avg7.toFixed(2)));
2025-12-20 orders 0 avg7: 0.71
2025-12-21 orders 2 avg7: 1.00
2025-12-22 orders 4 avg7: 1.43
2025-12-23 orders 1 avg7: 1.57
20 Aralık 0 siparişle geri geldi ve 21 Aralık'ın ortalaması SQL'deki avg_7_days gibi 1.00. bounds aralığı ayın tamamına yayıyor; "full" verseydin yalnız ilk ve son document arasını doldururdu ve ay başındaki siparişsiz günler eksik kalırdı. $fill yalnız sabit değerle değil, method: "locf" (son bilinen değeri taşımak) ya da "linear" (iki değer arasında doğrusal tahmin) ile de doldurabiliyor; stok seviyesi gibi bir ölçüm için locf, sipariş sayısı gibi bir sayım için 0 doğru seçim.
$graphLookup: Kategori Ağacı
Kategoriler parentId ile bir ağaç kuruyor: Fiction > Science Fiction > Dystopian. SQL Temelleri'nin CTE (Common Table Expression; sorgu içinde adı olan geçici sonuç kümesi) yazısında bu ağacı recursive CTE ile gezmiştik. $graphLookup aynı işi tek stage'de yapıyor: bir başlangıç değerinden çıkıp connectFromField ile connectToField arasındaki bağı tekrar tekrar izliyor.
// Her kategorinin yolu: parentId üzerinden yukarı çık
db.categories.aggregate([
{ $graphLookup: { from: "categories", startWith: "$parentId", connectFromField: "parentId",
connectToField: "_id", as: "ancestors", depthField: "level" } },
{ $set: { depth: { $size: "$ancestors" },
path: { $reduce: {
input: { $sortArray: { input: "$ancestors", sortBy: { level: -1 } } },
initialValue: "",
in: { $concat: ["$$value", "$$this.name", " > "] } } } } },
{ $set: { path: { $concat: ["$path", "$name"] } } },
{ $sort: { path: 1 } },
{ $limit: 8 }
]).forEach(c => print(String(c._id).padStart(2), c.depth, c.path));
// Alt ağaç: Non-Fiction (8) altındaki bütün kategoriler, sonra onlardaki kitaplar
const subtree = db.categories.aggregate([
{ $match: { _id: 8 } },
{ $graphLookup: { from: "categories", startWith: "$_id", connectFromField: "_id",
connectToField: "parentId", as: "descendants" } },
{ $project: { ids: { $concatArrays: [["$_id"], "$descendants._id"] } } }
]).next().ids;
print("categories under Non-Fiction:", subtree.sort((a, b) => a - b).join(", "),
" books:", db.books.countDocuments({ "category.categoryId": { $in: subtree } }));
13 0 Children
1 0 Fiction
2 1 Fiction > Classics
5 1 Fiction > Fantasy
6 1 Fiction > Mystery
7 1 Fiction > Poetry
3 1 Fiction > Science Fiction
4 2 Fiction > Science Fiction > Dystopian
categories under Non-Fiction: 8, 9, 10, 11, 12 books: 16
Yollar ve derinlikler SQL'deki recursive CTE'nin çıktısıyla aynı, Non-Fiction altındaki 16 kitap da. İki yön var: yukarı doğru (startWith: "$parentId", connectToField: "_id") atalar, aşağı doğru (startWith: "$_id", connectToField: "parentId") alt ağaç. $graphLookup sonucu bir array olarak döndürüyor ve sırayı garanti etmiyor; yolu kurmak için depthField ile gelen seviyeye göre $sortArray gerekti. Derinliği maxDepth ile sınırlayabilirsin. Stage her başlangıç document'i için yabancı collection'da connectToField'a göre tekrar tekrar arama yaptığı için o alanda index olmalı; 100 MB'ı aşınca geçici dosyaya yazıyor. Büyük grafikler için değil, kategori ağacı gibi küçük hiyerarşiler için uygun. Çok sık okunan bir ağaçta 5. yazıdaki yaklaşım daha ucuz: atalar listesini kategori document'ine yazmak.
$bucketAuto: Eşit Dağılımlı Aralıklar
7. yazıdaki $bucket sınırları senin verdiğin değerlerle çiziyordu. $bucketAuto'ya yalnız bucket sayısını veriyorsun, sınırları her bucket'a yaklaşık aynı sayıda document düşecek şekilde kendisi seçiyor:
// 08 - $bucketAuto: yaklaşık aynı sayıda kitap içeren fiyat aralıkları
db.books.aggregate([
{ $bucketAuto: { groupBy: "$listPrice", buckets: 4, output: { books: { $sum: 1 } } } }
]).forEach(b => print(`${b._id.min} - ${b._id.max}`.padEnd(18), "books", b.books));
200.00 - 350.00 books 20
350.00 - 470.00 books 19
470.00 - 660.00 books 20
660.00 - 1450.00 books 18
Aralıklar düzensiz ama dolulukları dengeli; fiyat filtresinin seçeneklerini katalogdaki gerçek dağılıma göre üretmek için kullanışlı. Sınırların "yuvarlak" olmasını istiyorsan granularity seçeneği (örneğin R5 ya da 1-2-5 serileri) sınırları bilinen sayı dizilerine oturtuyor.
$merge ile Materialized View
7. yazıda ağır raporları ekranın isteğine bağlamamayı önermiştik. Günlük satış özeti için sonucu bir collection'a yazıp ekranın oradan okuması gerekiyor: on-demand materialized view. $merge pipeline'ın sonucunu bir collection'la birleştiriyor; yalnız etkilenen günleri yeniden hesaplamak da mümkün.
// 08 - $merge: istek üzerine yenilenen bir materialized view olarak günlük satış özeti
function refresh(from) {
db.orders.aggregate([
{ $match: { status: { $ne: "Cancelled" }, orderedAt: { $gte: from } } },
{ $group: { _id: { $dateTrunc: { date: "$orderedAt", unit: "day" } },
orders: { $sum: 1 }, revenue: { $sum: "$total" } } },
{ $set: { refreshedAt: "$$NOW" } },
{ $merge: { into: "daily_sales", on: "_id", whenMatched: "replace", whenNotMatched: "insert" } }
]);
}
refresh(ISODate("2000-01-01")); // tam kurulum
show("full build");
// Yeni bir sipariş geliyor; yalnız etkilenen günler yeniden hesaplanır
db.orders.insertOne({ _id: 380, customer: { customerId: 3, name: "Grace Hopper" }, orderedAt: ISODate("2026-06-29T22:10:00Z"),
status: "Pending", items: [{ bookId: 26, title: "Dune", quantity: 1, unitPrice: Decimal128("390.50") }],
total: Decimal128("390.50"), currency: "TRY", payments: [] });
show("before refresh");
refresh(ISODate("2026-06-29"));
show("incremental refresh");
refresh(ISODate("2026-06-29"));
show("same refresh again");
full build days 273 2026-06-29: 1 orders 440.00
before refresh days 273 2026-06-29: 1 orders 440.00
incremental refresh days 273 2026-06-29: 2 orders 830.50
same refresh again days 273 2026-06-29: 2 orders 830.50
2026-06-29 2 830.50
2026-06-22 1 440.00
2026-06-20 1 290.90
İlk çağrı 273 günlük özeti kurdu. Yeni sipariş gelince özet bayat kaldı, refresh yalnız 29 Haziran'dan sonrasını yeniden hesaplayıp o günün document'ini değiştirdi; ikinci kez çalışması hiçbir şeyi bozmadı, yani yenileme idempotent. Ekran artık 379 siparişi değil, _id index'i üzerinden birkaç küçük document'i okuyor. Bedel bayatlık: özet son yenileme kadar güncel ve refreshedAt alanı bunu gösteriyor. Yenilemeyi bir zamanlayıcıya (her gece tam, her 5 dakikada son iki gün) ya da 10. yazıdaki change stream'e bağlamak senin kararın. Script sonda siparişi ve özet collection'ı siliyor.
$out daha basit bir kardeş: hedef collection'ı sonuçla baştan oluşturuyor ve iş bitince eskisinin yerine atomik olarak koyuyor; artımlı güncelleme yapamıyor ve sharded bir collection'a yazamıyor. Tam yenilenen küçük raporlar için $out, artımlı yenileme için $merge. on olarak _id dışında bir alan kullanacaksan hedef collection'da o alanlar üzerinde unique index olması gerekiyor.
Bellek Sınırları ve 9.0'ın Sorgu Başına Limiti
7. yazıda stage başına 100 MB sınırını ve sınırı aşan stage'in geçici dosyalara yazdığını (spill) söylemiştik. MongoDB 9.0 bunun üstüne bir sorgunun toplam belleğine sınır getirdi: varsayılan 1 GB ya da sunucu belleğinin %20'si, hangisi büyükse. Lab'da bu sınırı yalnız deneme için 20 MB'a indirip 30 MB'lık bir $group çalıştırdım:
// 08 - MongoDB 9.0: sorgu başına bellek sınırı (admin ister; sınır yalnız bu deneme için düşürülür)
const pipeline = [{ $group: { _id: "$bookId", payloads: { $push: "$payload" } } }];
const param = "internalQueryMaxMemoryUsageBytesPerOperation";
print("default limit:", Math.round(Number(original) / 1024 / 1024), "MB",
" peak memory of the $group:", Number(bookstore.memory_test.explain("executionStats").aggregate(pipeline).peakTrackedMemBytes), "bytes");
db.adminCommand({ setParameter: 1, [param]: 20 * 1024 * 1024 });
run("20 MB limit, allowDiskUse: false", { allowDiskUse: false });
run("20 MB limit, allowDiskUse: true ", { allowDiskUse: true });
db.adminCommand({ setParameter: 1, [param]: original });
run("default, allowDiskUse: false", { allowDiskUse: false });
default limit: 1567 MB peak memory of the $group: 30225118 bytes
20 MB limit, allowDiskUse: false: QueryExceededMemoryLimitNoDiskUseAllowed
20 MB limit, allowDiskUse: true : ok, 77 groups
default, allowDiskUse: false: ok, 77 groups
memory_test collection'ı script'in ürettiği 30.000 document'lik, document başına 1 KB'lık geçici veri. Varsayılan sınır bu lab'da 1567 MB, yani Docker'ın sanal makinesine ayrılan belleğin %20'si; senin makinende farklı çıkar. explain'in 9.0'da gelen peakTrackedMemBytes alanı $group'un 30 MB kullandığını gösteriyor: stage sınırının (100 MB) altında, ama indirdiğimiz sorgu sınırının üstünde. allowDiskUse: false iken sorgu QueryExceededMemoryLimitNoDiskUseAllowed ile durdu; allowDiskUse: true iken stage diske yazıp bitirdi. Sınır varsayılana dönünce aynı sorgu yine çalıştı. Pratikteki anlamı: tek bir pahalı rapor artık sunucunun bütün belleğini tüketemiyor, ama çok büyük bir sorgu 9.0'a geçince ilk kez bu hatayı alabilir. Çare, dokümanın da önerdiği gibi, filtreyi öne almak, $sort'un arkasına $limit koymak ve sıralamayı index'e yaptırmak.
MongoDB 8.0'da farkı: parametre yok. 8.0.38'de getParameter çağrısı no option found to get hatası verdi ve aynı $group allowDiskUse: false ile de çalıştı, çünkü 30 MB stage sınırının altında.
Server-Side JavaScript: $function
$function ve $accumulator pipeline içinde JavaScript çalıştırıyor. 8.0'da deprecated ilan edilmişlerdi; 9.0 release notes'una göre deprecation kaldırıldı ve JavaScript artık WebAssembly (WASM; tarayıcı dışında da çalışan, sandbox'lı bir ikili kod formatı) tabanlı bir motorda, daha sıkı izolasyonla çalışıyor. kitapların ISBN (13 haneli uluslararası standart kitap numarası) kontrol hanesini hem JavaScript'le hem aggregation operator'leriyle doğrulayalım:
// 08 - $function ile server-side JavaScript ve native ifade: ISBN-13 kontrol hanesi
const viaFunction = db.books.aggregate([
{ $set: { valid: { $function: {
body: function (isbn) {
let sum = 0;
for (let i = 0; i < 12; i++) sum += Number(isbn[i]) * (i % 2 === 0 ? 1 : 3);
return (10 - sum % 10) % 10 === Number(isbn[12]);
},
args: ["$isbn"],
lang: "js"
} } } },
{ $group: { _id: "$valid", books: { $sum: 1 } } }
]).toArray();
// Aynı kural aggregation operator'leriyle: JavaScript motoru devrede değil
const viaOperators = db.books.aggregate([
{ $set: { sum: { $reduce: {
input: { $range: [0, 12] },
initialValue: 0,
in: { $add: ["$$value", { $multiply: [
{ $toInt: { $substrCP: ["$isbn", "$$this", 1] } },
{ $cond: [{ $eq: [{ $mod: ["$$this", 2] }, 0] }, 1, 3] }] }] }
} } } },
{ $set: { valid: { $eq: [{ $mod: [{ $subtract: [10, { $mod: ["$sum", 10] }] }, 10] },
{ $toInt: { $substrCP: ["$isbn", 12, 1] } }] } } },
{ $group: { _id: "$valid", books: { $sum: 1 } } }
]).toArray();
$function: [{"_id":true,"books":77}]
operators: [{"_id":true,"books":77}]
İki yol da 77 ISBN'i geçerli buldu. 9.0.8'in log'unda ilk çağrıda "Initialized shared WASM engine context" satırı çıktı (98 ms), ama aynı çağrı için $function is deprecated uyarısı (id 8996503) da yazıldı; release notes ile log mesajı bu patch'te henüz uyuşmuyor. 8.0.38 aynı uyarıyı yazıp sorguyu çalıştırdı. Deprecation kalksa da kural değişmiyor: doküman $function'ın performansı düşürebileceğini ve yalnız operator'lerle ifade edilemeyen durumlar için kullanılmasını söylüyor. JavaScript optimizer için kara kutu, query engine'e taşınamıyor ve bir de 9.0'a özgü ayrıntı var: JavaScript içindeki yerel Date işlemleri artık sunucunun saat diliminden bağımsız olarak UTC (eşgüdümlü evrensel zaman) ile çalışıyor. Operator'lerle yazılan sürüm uzun görünse de tercih edilmesi gereken o.
Üretimde Dikkat
Raporlama yükünü primary'ye bindirme. Aylık ciro, top-N ve materialized view yenilemesi gibi ağır pipeline'lar primary'nin CPU'sunu, belleğini ve WiredTiger cache'ini uygulamanın yazmalarıyla paylaşır. Seçenekler: raporu read preference ile secondary'den okumak (bayatlık kabul edilerek, 12. yazı), raporlar için ayrılmış ve uygulamanın görmediği bir member (hidden ya da tag'li analitik node, 13. yazı) ya da materialized view'ları sakin saatlerde yenilemek.
Materialized view'ı da bir collection gibi yönet. Okuma deseni neyse ona index koy, yenilemenin süresini ve son başarılı yenileme zamanını izle; refreshedAt gibi bir alan ekranın "veriler 5 dakika önce güncellendi" demesini sağlar. Yenileme yarıda kalırsa $merge o ana kadar yazdıklarını geri almaz; bu yüzden yenileme idempotent olmalı ki tekrar çalıştırmak her zaman güvenli olsun.
Sık Düşülen Tuzaklar
Eksik günleri unutmak. "Önce Tahmin Et"teki gibi $group olmayan günler için document üretmez; günlük grafikte boşluk, hareketli ortalamada yanlış değer ve "satışın olmadığı günler" raporunda eksik satır demek. Zaman serisi raporlarında $densify ve $fill adımını varsayılan say.
Pencere çerçevesini yazmamak. $setWindowFields'te window vermezsen $sum gibi operator'ler bütün partition'ı kullanır, sortBy olsa bile; running total yerine her satırda toplam çıkar. Çerçeveyi her zaman açıkça yaz.
$out'u artımlı yenileme için kullanmak. $out her çalışmada hedefi baştan kurar; 273 günlük özeti tek bir yeni sipariş için yeniden hesaplamak küçük veride fark edilmez, yıllarca birikmiş veride her yenilemeyi bir tam taramaya çevirir.
Küçük Bir Deneme
★☆☆ Her müşterinin siparişlerini tarih sırasıyla numaralandır ($documentNumber) ve bir önceki siparişle arasındaki gün farkını hesapla ($shift). Ada Lovelace'in en uzun ara verdiği dönemi bul.
★★☆ 2025'in her haftası için sipariş sayısını $densify ile eksiksiz üret ve 4 haftalık hareketli ortalamayı hesapla. Haftayı $dateTrunc ile Pazartesi'den başlat (startOfWeek).
★★★ daily_sales'e ek olarak kategori ve gün bazında bir materialized view kur (on iki alan olsun; gereken unique index'i ekle). Bir siparişin durumu Cancelled olduğunda hangi document'lerin yeniden hesaplanması gerektiğini belirleyen ve yalnız onları yenileyen bir script yaz.
Bu yazıyla mongosh tarafındaki temel bitti. Bir sonraki yazıda .NET'e geçiyoruz: MongoClient'ın DI (dependency injection; bağımlılıkların dışarıdan verilmesi) ile singleton kaydı, POCO (hiçbir framework'e bağlı olmayan düz C# class'ı) mapping ve convention'lar, decimal, Guid ve DateTime'ın BSON (MongoDB'nin ikili document formatı) karşılıkları ve LINQ (C#'ta sorguyu dilin içinde yazmayı sağlayan özellik) ile sorgu.