Dataset 2000 transaksi kini menunggu pertanyaan. Bagian ini menyusun tiga skrip laporan — omzet, produk, tren — yang masing-masing adalah rangkaian aggregation pipeline di atas collection transaksi. Seluruh stage yang dipakai ($unwind, $group, $dateTrunc, $setWindowFields) sudah diperkenalkan di Section Aggregation; fokus kita sekarang adalah menyusunnya menjadi laporan yang menjawab pertanyaan bisnis, lalu membaca angkanya seperti seorang analis.

Dua Jenis Grup dalam Satu Collection #

Perhatikan dulu di level mana setiap laporan mengelompokkan data — keputusan ini menentukan bentuk pipelinenya:

LaporanKunci grupPipeline inti
Omzet per kategoriitems.kategori (level item)$unwind → $group → $sort
Omzet per bulan / hariantanggal (level transaksi)$dateTrunc → $group → $sort
Peringkat produkitems.produk (level item)$unwind → $group → $sort → $limit
Tren bulananbulan (level transaksi)$group → $setWindowFields

Kategori dan judul produk hidup di dalam array items, jadi pipeline-nya wajib membongkar array lebih dulu dengan $unwind. Sebaliknya field total tersimpan di level transaksi berdasarkan keputusan desain Bagian 1 — laporan bulanan dan harian cukup menjumlahkan $total tanpa membongkar apa pun. Satu collection, dua sudut pandang, tanpa duplikasi data.

Satu catatan tentang $dateTrunc: stage ini mengembalikan nilai bertipe Date, bukan teks. Menjadikannya kunci grup membuat hasil laporan tetap bisa diurutkan kronologis dan diformat ulang kapan pun; kalau kita langsung memakai $dateToString sebagai kunci, urutan menjadi urutan alfabetis teks — aman untuk format %Y-%m, tetapi menyesatkan begitu formatnya berubah.

Laporan Omzet #

js
// laporan-omzet.js — omzet per kategori, per bulan, dan harian
// Jalankan setelah dataset-seed.js:
//   mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-omzet.js

var ana = db.getSiblingDB("proyek_analitik")

function omzetPerKategori() {
  print("== omzet per kategori ==")
  ana.transaksi.aggregate([
    { $unwind: "$items" },
    { $group: {
        _id: "$items.kategori",
        unit: { $sum: "$items.jumlah" },
        omzet: { $sum: { $multiply: ["$items.harga", "$items.jumlah"] } },
    } },
    { $sort: { omzet: -1 } },
    { $project: { _id: 0, kategori: "$_id", unit: 1, omzet: 1 } },
  ]).forEach(function (r) {
    print(" ", r.kategori, "| unit", r.unit, "| omzet", r.omzet)
  })
}

function omzetPerBulan() {
  print("== omzet per bulan ==")
  ana.transaksi.aggregate([
    { $group: {
        _id: { $dateTrunc: { date: "$tanggal", unit: "month" } },
        transaksi: { $sum: 1 },
        omzet: { $sum: "$total" },
    } },
    { $sort: { _id: 1 } },
    { $project: {
        _id: 0,
        bulan: { $dateToString: { format: "%Y-%m", date: "$_id" } },
        transaksi: 1, omzet: 1,
    } },
  ]).forEach(function (r) {
    print(" ", r.bulan, "| transaksi", r.transaksi, "| omzet", r.omzet)
  })
}

function omzetHarian(dari, sampai) {
  print("== omzet harian ==")
  ana.transaksi.aggregate([
    { $match: { tanggal: { $gte: dari, $lt: sampai } } },
    { $group: {
        _id: { $dateTrunc: { date: "$tanggal", unit: "day" } },
        transaksi: { $sum: 1 },
        omzet: { $sum: "$total" },
    } },
    { $sort: { _id: 1 } },
    { $project: {
        _id: 0,
        hari: { $dateToString: { format: "%Y-%m-%d", date: "$_id" } },
        transaksi: 1, omzet: 1,
    } },
  ]).forEach(function (r) {
    print(" ", r.hari, "| transaksi", r.transaksi, "| omzet", r.omzet)
  })
}

omzetPerKategori()
omzetPerBulan()
omzetHarian(ISODate("2026-06-24T00:00:00Z"), ISODate("2026-07-01T00:00:00Z"))

Jalankan setelah dataset-seed.js:

bash
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-omzet.js

Output yang Dihasilkan:

text
== omzet per kategori ==
  devops | unit 3080 | omzet 490354000
  database | unit 3001 | omzet 453185000
  pemrograman | unit 3047 | omzet 451730000
  bisnis | unit 3096 | omzet 412945000
== omzet per bulan ==
  2026-01 | transaksi 362 | omzet 333395000
  2026-02 | transaksi 295 | omzet 259248000
  2026-03 | transaksi 331 | omzet 308835000
  2026-04 | transaksi 334 | omzet 296978000
  2026-05 | transaksi 340 | omzet 304363000
  2026-06 | transaksi 338 | omzet 305395000
== omzet harian ==
  2026-06-24 | transaksi 10 | omzet 7486000
  2026-06-25 | transaksi 15 | omzet 13792000
  2026-06-26 | transaksi 12 | omzet 12934000
  2026-06-27 | transaksi 13 | omzet 14191000
  2026-06-28 | transaksi 14 | omzet 13044000
  2026-06-29 | transaksi 13 | omzet 11563000
  2026-06-30 | transaksi 10 | omzet 8145000

Bedah Pipeline Omzet #

  1. omzetPerKategori() membuka dengan $unwind: "$items" — tanpa stage ini, $group hanya melihat document transaksi dan $items.kategori berupa array, bukan nilai yang bisa dijadikan kunci. Setelah terbongkar, accumulator $sum menjumlahkan harga × jumlah per baris item lewat $multiply, dan $sort: { omzet: -1 } menempatkan kategori teratas lebih dulu.
  2. omzetPerBulan() memakai $dateTrunc sebagai kunci grup: setiap tanggal dipotong ke awal bulanya (misalnya 2026-03-17T14.20 menjadi 2026-03-01T00.00), sehingga seluruh transaksi satu bulan jatuh ke satu nilai _id yang sama. Stage $project kemudian memformat Date tersebut menjadi teks "2026-03" lewat $dateToString — pemformatan tampilan dikerjakan di akhir, datanya tetap bertipe Date.
  3. omzetHarian() menerima parameter rentang lalu meletakkan $match sebelum $group. Urutan ini penting: menyaring 43 transaksi pekan terakhir lebih dulu membuat $group hanya menerima bahan yang relevan. Menempatkan $match paling awal adalah kebiasaan terbaik merangkai pipeline — semakin dulu data menyusut, semakin ringan stage berikutnya.

Angkanya sendiri menyimpan satu pelajaran: kategori bisnis justru memimpin jumlah unit (3.096) tetapi menutup daftar omzet (Rp412.945.000) karena bukunya paling terjangkau. Unit dan omzet adalah dua ukuran yang berbeda — pilih yang sesuai dengan pertanyaan yang diajukan.

Laporan Produk #

js
// laporan-produk.js — peringkat produk, kontribusi omzet, dan top-N
// Jalankan setelah dataset-seed.js:
//   mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-produk.js

var ana = db.getSiblingDB("proyek_analitik")

// Satu baris per produk: bongkar array items lalu rekap per judul buku.
function omzetPerProduk() {
  return ana.transaksi.aggregate([
    { $unwind: "$items" },
    { $group: {
        _id: "$items.produk",
        unit: { $sum: "$items.jumlah" },
        omzet: { $sum: { $multiply: ["$items.harga", "$items.jumlah"] } },
    } },
    { $sort: { omzet: -1, produk: 1 } },
    { $project: { _id: 0, produk: "$_id", unit: 1, omzet: 1 } },
  ]).toArray()
}

function produkTerlaris(n) {
  print("==", n, "produk terlaris ==")
  const semua = omzetPerProduk()
  const total = semua.reduce(function (a, r) { return a + r.omzet }, 0)
  semua.slice(0, n).forEach(function (r, i) {
    const persen = Math.round((r.omzet / total) * 1000) / 10
    print(" ", (i + 1) + ".", r.produk, "| unit", r.unit, "| omzet", r.omzet, "| kontribusi", persen + "%")
  })
  print("  (total omzet seluruh produk:", total + ")")
}

function penjualanTerendah(n) {
  print("==", n, "produk dengan omzet terendah ==")
  const semua = omzetPerProduk()
  semua.slice(-n).reverse().forEach(function (r, i) {
    print(" ", (i + 1) + ".", r.produk, "| unit", r.unit, "| omzet", r.omzet)
  })
}

produkTerlaris(5)
penjualanTerendah(3)

Jalankan dengan pola yang sama:

bash
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-produk.js

Output yang Dihasilkan:

text
== 5 produk terlaris ==
  1. Kubernetes di Produksi | unit 1097 | omzet 199654000 | kontribusi 11%
  2. MongoDB untuk Pengembang | unit 975 | omzet 170625000 | kontribusi 9.4%
  3. Rust untuk Sistem Andal | unit 998 | omzet 164670000 | kontribusi 9.1%
  4. Desain Skema Document | unit 1056 | omzet 158400000 | kontribusi 8.8%
  5. Pemrograman Modern dengan Go | unit 1037 | omzet 153476000 | kontribusi 8.5%
  (total omzet seluruh produk: 1808214000)
== 3 produk dengan omzet terendah ==
  1. SQL dan Relational Dasar | unit 970 | omzet 124160000
  2. Analisis Keuangan Praktis | unit 1039 | omzet 125719000
  3. TypeScript dalam Praktik | unit 1012 | omzet 133584000

Skrip ini memperlihatkan pembagian kerja yang sehat antara pipeline dan JavaScript. Pipeline bertugas mengelompokkan dan mengurutkan — omzetPerProduk() mengembalikan array 12 baris lewat toArray(). Sisanya pekerjaan presentasi: slice(0, n) memotong top-N, reduce menjumlahkan total, dan Math.round membulatkan kontribusi ke satu angka desimal. Top-N pun bisa juga dikerjakan di pipeline dengan $limit, tetapi memotong di sisi klien membuat satu panggilan omzetPerProduk() bisa melayani berbagai laporan — penjualanTerendah() bahkan memakai slice(-n) pada array yang sama tanpa menjalankan aggregation kedua kali.

Perhatikan pula $sort: { omzet: -1, produk: 1 } — kunci kedua menjadi pemutus seri saat dua produk kebetulan beromzet sama, sehingga urutan hasil stabil antarjalankan. Kontribusi 11% untuk Kubernetes di Produksi berasal dari 199654000 / 1808214000: satu dari dua belas produk menyumbang sepertiga lebih dari porsi rata-rata (8,3%), pertanda katalog yang cukup seimbang.

Laporan Tren #

Dua laporan sebelumnya memotret satu titik waktu. Tren menuntut perbandingan antarwaktu — pekerjaan yang tepat untuk $setWindowFields:

js
// laporan-tren.js — tren bulanan dengan $setWindowFields: delta dan rata-rata bergerak
// Jalankan setelah dataset-seed.js:
//   mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-tren.js

var ana = db.getSiblingDB("proyek_analitik")

function trenBulanan() {
  print("== tren omzet bulanan (delta vs bulan sebelumnya) ==")
  ana.transaksi.aggregate([
    { $group: {
        _id: { $dateTrunc: { date: "$tanggal", unit: "month" } },
        transaksi: { $sum: 1 },
        omzet: { $sum: "$total" },
    } },
    { $sort: { _id: 1 } },
    { $setWindowFields: {
        sortBy: { _id: 1 },
        output: {
          omzetLalu: { $shift: { output: "$omzet", by: -1, default: null } },
          rata3Bulan: { $avg: "$omzet", window: { documents: [-2, 0] } },
        },
    } },
    { $set: { delta: { $subtract: ["$omzet", "$omzetLalu"] } } },
    { $project: {
        _id: 0,
        bulan: { $dateToString: { format: "%Y-%m", date: "$_id" } },
        omzet: 1,
        delta: 1,
        persen: { $round: [{ $multiply: [{ $divide: ["$delta", "$omzetLalu"] }, 100] }, 1] },
        rata3Bulan: 1,
    } },
  ]).forEach(function (r) {
    print(" ", r.bulan,
      "| omzet", r.omzet,
      "| delta", r.delta === null ? "-" : r.delta,
      "(" + (r.persen === null ? "-" : r.persen + "%") + ")",
      "| rata3", Math.round(r.rata3Bulan))
  })
}

function trenKategori(kategori) {
  print("== tren bulanan kategori:", kategori, "==")
  ana.transaksi.aggregate([
    { $unwind: "$items" },
    { $match: { "items.kategori": kategori } },
    { $group: {
        _id: { $dateTrunc: { date: "$tanggal", unit: "month" } },
        omzet: { $sum: { $multiply: ["$items.harga", "$items.jumlah"] } },
    } },
    { $sort: { _id: 1 } },
    { $setWindowFields: {
        sortBy: { _id: 1 },
        output: {
          omzetLalu: { $shift: { output: "$omzet", by: -1, default: null } },
        },
    } },
    { $set: { delta: { $subtract: ["$omzet", "$omzetLalu"] } } },
    { $project: {
        _id: 0,
        bulan: { $dateToString: { format: "%Y-%m", date: "$_id" } },
        omzet: 1,
        delta: 1,
    } },
  ]).forEach(function (r) {
    print(" ", r.bulan, "| omzet", r.omzet, "| delta", r.delta === null ? "-" : r.delta)
  })
}

trenBulanan()
trenKategori("devops")
bash
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-tren.js

Output yang Dihasilkan:

text
== tren omzet bulanan (delta vs bulan sebelumnya) ==
  2026-01 | omzet 333395000 | delta - (-) | rata3 333395000
  2026-02 | omzet 259248000 | delta -74147000 (-22.2%) | rata3 296321500
  2026-03 | omzet 308835000 | delta 49587000 (19.1%) | rata3 300492667
  2026-04 | omzet 296978000 | delta -11857000 (-3.8%) | rata3 288353667
  2026-05 | omzet 304363000 | delta 7385000 (2.5%) | rata3 303392000
  2026-06 | omzet 305395000 | delta 1032000 (0.3%) | rata3 302245333
== tren bulanan kategori: devops ==
  2026-01 | omzet 97752000 | delta -
  2026-02 | omzet 75330000 | delta -22422000
  2026-03 | omzet 86006000 | delta 10676000
  2026-04 | omzet 79790000 | delta -6216000
  2026-05 | omzet 75816000 | delta -3974000
  2026-06 | omzet 75660000 | delta -156000

Bedah Window Function #

  1. $setWindowFields dengan sortBy: { _id: 1 } menegaskan urutan pemrosesan: keenam baris bulanan dibaca dari Januari ke Juni. Seluruh operator window bekerja relatif terhadap urutan ini.
  2. $shift dengan by: -1 menyalin nilai omzet dari satu document sebelumnya ke field omzetLalu. Baris pertama tidak punya tetangga, jadi default: null mengisinya; $subtract terhadap null menghasilkan null, dan itulah mengapa Januari tampil sebagai delta -. Stage $set berikutnya menghitung delta, lalu $project membaginya dengan omzetLalu untuk persentase perubahan.
  3. rata3Bulan memakai window: { documents: [-2, 0] } — jendela tiga document yang berakhir di baris saat ini. Januari dirata-rata sendirian, Februari bersama Januari, dan mulai Maret jendela penuh tiga bulan terisi. Rata-rata bergerak seperti ini meratakan lonjakan sesaat; perhatikan bagaimana rata3 Maret (Rp300,5 juta) jauh lebih tenang daripada lompatan delta-nya (+19,1%).
  4. trenKategori() menggabungkan seluruh teknik bagian ini untuk satu irisan data — dan menyimpan satu jebakan klasik.

Tren kategori devops layak dibaca teliti: omzet tertinggi di Januari (Rp97.752.000) tidak pernah kembali, dan delta mengecil menuju Juni (−Rp156.000) — posisi yang mendatar, bukan yang pulih. Laporan seperti inilah yang biasanya memicu pertanyaan lanjutan: promosi, stok, atau pergeseran minat pembaca.

Apa Berikutnya #

Ketiga laporan berjalan, tetapi setiap $match tanggal dan $sort masih memindai seluruh 2000 document — terasa cepat hari ini, melambat seiring data tumbuh. Bagian 3 memasang index yang tepat, membuktikan perubahannya lewat explain(), dan membahas allowDiskUse serta proyeksi untuk pipeline yang lebih besar.