Belajar MongoDB - Advanced Aggregation: Lookups, Facets & Window Functions
Episode 10 of 21

Belajar MongoDB - Advanced Aggregation: Lookups, Facets & Window Functions

Menghubungkan collection dengan $lookup, menjalankan beberapa sub-pipeline sekaligus dengan $facet, membuat histogram dengan $bucket, serta menghitung ranking, running totals, dan moving average memakai $setWindowFields ala window functions di SQL.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Pendahuluan

Di episode 9 kalian membangun fondasi aggregation pipeline. Tapi aplikasi nyata butuh lebih dari sekedar group dan sum: menggabungkan data lintas collection, menghitung peringkat sekaligus ringkasan dalam satu query, dan membuat histogram distribusi harga.

Episode 10 menjawabnya dengan stage-stage lanjutan: $lookup untuk join antar collection, $facet untuk banyak sub-pipeline paralel, $bucket untuk histogram, dan $setWindowFields untuk window functions ala SQL. Mari mulai.

$lookup: Menggabungkan Collection

Basic Lookup

$lookup adalah analog LEFT JOIN di SQL: ia mengambil field dari dokumen collection utama, mencocokkannya dengan field di collection lain, dan menempelkan hasilnya sebagai array ke dokumen utama.

Basic lookup dari orders ke users
db.orders.aggregate([
  { $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "user"
  } }
])

Bacanya: dari collection users, cocokkan userId di dokumen order dengan _id di users, lalu simpan hasilnya ke field user (selalu array, meski satu dokumen). Setiap order kini membawa data user lengkap — tanpa dua query terpisah. Karena as selalu array, rapikan dengan $unwind bila kalian butuh bentuk flat.

Correlated Subquery Lookup

Basic lookup hanya bisa mencocokkan dengan kesetaraan sederhana. Untuk kondisi lebih kompleks — seperti menggabungkan berdasarkan dua field atau filter tambahan — gunakan pipeline syntax di let + pipeline:

Correlated lookup dengan kondisi tambahan
db.orders.aggregate([
  { $lookup: {
      from: "items",
      let: { orderId: "$_id" },
      pipeline: [
        { $match: {
            $expr: { $eq: ["$orderId", "$$orderId"] },
            qty: { $gte: 2 }
        } }
      ],
      as: "bulkItems"
  } }
])

Sintaks let mendefinisikan variabel lokal ($$orderId) yang dipakai di sub-pipeline — memberi kontrol penuh atas kondisi join.

$facet: Multi-Pipeline dalam Satu Query

$facet menjalankan beberapa sub-pipeline secara paralel dalam satu pass atas data — hasilnya dikumpulkan dalam satu dokumen. Ini revolusioner untuk halaman dashboard yang membutuhkan beberapa ringkasan berbeda dari data yang sama, tanpa query berulang:

Satu query menghasilkan beberapa ringkasan
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $facet: {
      "totalRevenue": [
        { $group: { _id: null, total: { $sum: "$total" } } }
      ],
      "byCategory": [
        { $group: { _id: "$category", count: { $sum: 1 } } },
        { $sort: { count: -1 } }
      ],
      "recentOrders": [
        { $sort: { createdAt: -1 } },
        { $limit: 5 },
        { $project: { _id: 1, total: 1, status: 1 } }
      ]
  } }
])

Hasilnya satu dokumen dengan tiga key: totalRevenue, byCategory, dan recentOrders. Bayangkan berapa banyak query terpisah yang dihemat — dan konsistensinya sempurna karena semua facet membaca snapshot yang sama.

$bucket dan $bucketAuto

$bucket mengelompokkan data ke dalam rentang (bucket) yang kalian tentukan. Sempurna untuk membuat histogram harga:

Bucket harga dengan batas manual
db.products.aggregate([
  { $bucket: {
      groupBy: "$price",
      boundaries: [0, 100000, 250000, 500000, 1000000],
      default: "lainnya",
      output: {
        count: { $sum: 1 },
        totalValue: { $sum: "$price" }
      }
  } }
])

Batas [0, 100000, 250000, 500000, 1000000] membentuk empat bucket: 0–100rb, 100rb–250rb, 250rb–500rb, 500rb–1jt. Produk di luar rentang masuk ke bucket default.

Sementara $bucketAuto membagi data ke jumlah bucket yang diinginkan dengan batas yang dihitung otomatis agar merata:

Bucket otomatis dalam 5 kelompok
db.products.aggregate([
  { $bucketAuto: {
      groupBy: "$price",
      buckets: 5,
      output: {
        count: { $sum: 1 },
        average: { $avg: "$price" }
      }
  } }
])

MongoDB menentukan batas tiap bucket sendiri sehingga distribusinya merata — berguna saat rentang alami data belum diketahui.

$setWindowFields: Window Functions

$setWindowFields menghadirkan kemampuan window functions ala SQL (ROW_NUMBER, SUM ... OVER, moving average) ke MongoDB. Ia menghitung nilai berdasarkan window — jendela dokumen di sekitar dokumen saat ini — tanpa mengelompokkan seluruh data.

Ranking dengan $rank dan $denseRank

Ranking produk berdasarkan penjualan
db.products.aggregate([
  { $setWindowFields: {
      partitionBy: "$category",
      sortBy: { soldCount: -1 },
      output: {
        rank: { $rank: {} },
        denseRank: { $denseRank: {} }
      }
  } },
  { $match: { rank: { $lte: 3 } } },
  { $project: { name: 1, category: 1, soldCount: 1, rank: 1 } }
])

partitionBy memecah data per kategori, sortBy menentukan urutan, lalu $rank memberi nomor urut. Beda $rank dan $denseRank: $rank melompat setelah nilai seri (1, 2, 2, 4), $denseRank tidak (1, 2, 2, 3). Hasilnya tiga produk terlaris per kategori.

Running Total dan Moving Average

Window functions juga bisa menjumlahkan kumulatif dan menghitung rata-rata bergerak — jantung analisis time-series:

Running total dan moving average penjualan
db.sales.aggregate([
  { $sort: { date: 1 } },
  { $setWindowFields: {
      sortBy: { date: 1 },
      output: {
        runningTotal: {
          $sum: "$amount",
          window: { documents: ["unbounded", "current"] }
        },
        movingAvg7: {
          $avg: "$amount",
          window: { documents: [-6, 0] }
        }
      }
  } }
])
  • runningTotal menjumlahkan dari dokumen pertama (unbounded) sampai saat ini (current); movingAvg7 merata-rata 7 hari terakhir untuk menghaluskan fluktuasi harian.

Gabungan $facet dan $setWindowFields menghasilkan laporan level enterprise dalam satu query: satu facet untuk ringkasan agregat, satu lagi untuk detail dengan ranking dan running totals. Semua facet berbagi snapshot yang sama, jadi hasil dijamin konsisten.

Yang perlu diwaspadai: $lookup yang tidak di-index sangat mahal — ia menjadi nested loop di server. Pastikan field foreignField punya index, dan hindari $lookup raksasa di setiap request; untuk workload read-heavy, pertimbangkan snapshot/denormalisasi dari episode 7. Gunakan $lookup sebagai pelengkap, bukan tulang punggung desain.

Penutup

Pada episode 10 ini kalian telah mendorong aggregation ke level profesional: $lookup basic dan correlated subquery untuk menggabungkan collection ala LEFT JOIN, $facet untuk menjalankan beberapa sub-pipeline paralel dalam satu query — seperti dashboard lengkap dalam satu panggilan, $bucket dan $bucketAuto untuk histogram dengan batas manual atau otomatis, serta $setWindowFields untuk window functions: ranking dengan $rank/$denseRank, running total, dan moving average.

Inti yang harus dibawa pulang:

  • $lookup menggabungkan collection; selalu pastikan foreignField ter-index.
  • Pipeline syntax $lookup memberi kondisi join yang fleksibel.
  • $facet menggabungkan banyak ringkasan dalam satu query konsisten.
  • $bucket untuk histogram manual; $bucketAuto untuk distribusi otomatis.
  • $setWindowFields memberi ranking, running totals, dan moving average.

Di episode 11 berikutnya kita bicara cara menemukan data dengan kata kunci: Full-Text Search & Atlas Search. Kalian akan membuat text index dan memakai query $text dengan scoring relevansi, lalu mengenal MongoDB Atlas Search berbasis Lucene yang menghadirkan fuzzy search, autocomplete, dan faceted search. Sampai jumpa di episode 11!