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:
| Laporan | Kunci grup | Pipeline inti |
|---|---|---|
| Omzet per kategori | items.kategori (level item) | $unwind → $group → $sort |
| Omzet per bulan / harian | tanggal (level transaksi) | $dateTrunc → $group → $sort |
| Peringkat produk | items.produk (level item) | $unwind → $group → $sort → $limit |
| Tren bulanan | bulan (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 #
// 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:
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-omzet.jsOutput yang Dihasilkan:
== 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 8145000Bedah Pipeline Omzet #
omzetPerKategori()membuka dengan$unwind: "$items"— tanpa stage ini,$grouphanya melihat document transaksi dan$items.kategoriberupa array, bukan nilai yang bisa dijadikan kunci. Setelah terbongkar, accumulator$summenjumlahkanharga × jumlahper baris item lewat$multiply, dan$sort: { omzet: -1 }menempatkan kategori teratas lebih dulu.omzetPerBulan()memakai$dateTruncsebagai kunci grup: setiaptanggaldipotong ke awal bulanya (misalnya2026-03-17T14.20menjadi2026-03-01T00.00), sehingga seluruh transaksi satu bulan jatuh ke satu nilai_idyang sama. Stage$projectkemudian memformatDatetersebut menjadi teks"2026-03"lewat$dateToString— pemformatan tampilan dikerjakan di akhir, datanya tetap bertipe Date.omzetHarian()menerima parameter rentang lalu meletakkan$matchsebelum$group. Urutan ini penting: menyaring 43 transaksi pekan terakhir lebih dulu membuat$grouphanya menerima bahan yang relevan. Menempatkan$matchpaling 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 #
// 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:
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-produk.jsOutput yang Dihasilkan:
== 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 133584000Skrip 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:
// 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")mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/laporan-tren.jsOutput yang Dihasilkan:
== 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 -156000Bedah Window Function #
$setWindowFieldsdengansortBy: { _id: 1 }menegaskan urutan pemrosesan: keenam baris bulanan dibaca dari Januari ke Juni. Seluruh operator window bekerja relatif terhadap urutan ini.$shiftdenganby: -1menyalin nilaiomzetdari satu document sebelumnya ke fieldomzetLalu. Baris pertama tidak punya tetangga, jadidefault: nullmengisinya;$subtractterhadapnullmenghasilkannull, dan itulah mengapa Januari tampil sebagaidelta -. Stage$setberikutnya menghitungdelta, lalu$projectmembaginya denganomzetLaluuntuk persentase perubahan.rata3Bulanmemakaiwindow: { 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 bagaimanarata3Maret (Rp300,5 juta) jauh lebih tenang daripada lompatandelta-nya (+19,1%).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.