Toko buku daring mencatat transaksi setiap hari, tetapi tumpukan document transaksi belum menjawab pertanyaan bisnis: kategori mana yang menyumbang omzet terbesar, produk apa yang paling laku, dan ke arah mana tren bulanan bergerak. Proyek dashboard analitik menjawab ketiganya hanya dengan mongosh dan aggregation pipeline. Kita memosisikan diri sebagai analis data: menyiapkan dataset yang dapat direproduksi, menyusun laporan, lalu mengoptimalkan pipeline agar tetap tangkas saat data membesar. Semuanya dimulai dari satu collection bernama transaksi di database proyek_analitik.
Desain Collection transaksi #
Satu transaksi penjualan disimpan sebagai satu document. Rincian buku yang dibeli menjadi array items di dalam document tersebut — pola yang tepat karena rincian tidak pernah dibaca terpisah dari transaksinya:
| Field | Tipe | Isi |
|---|---|---|
tanggal | Date | Waktu transaksi, tersebar 1 Januari – 30 Juni 2026 |
kota | string | Salah satu dari 5 kota cabang |
channel | string | "toko", "online", atau "aplikasi" |
items | array | 1–3 buku, tiap elemen { produk, kategori, harga, jumlah } |
total | number | Penjumlahan harga × jumlah seluruh item |
Dua keputusan desain di sini menentukan bentuk laporan pada bagian berikutnya. Pertama, kategori dan harga disalin ke dalam setiap item sebagai potret saat transaksi terjadi — harga katalog dapat berubah, sedangkan laporan historis harus memakai harga saat penjualan. Kedua, field total sengaja dihitung dan disimpan saat penyimpanan; laporan omzet per bulan cukup menjumlahkan total tanpa membongkar array, sementara laporan per kategori dan per produk tetap membongkar items karena informasinya memang hidup di level item.
Apakah items layak di-embed dan bukan dikirim ke collection terpisah? Aturannya mengikuti pola akses: rincian belanja selalu dibaca bersama transaksinya, dan ukuran array kita 1–3 elemen — jauh dari risiko document growth. Pola ini tetap sehat untuk belanja puluhan item; toko grosir yang satu nota bisa memuat ratusan baris baru perlu memindahkan baris belanja ke collection sendiri.
Angka Acak yang Bisa Direproduksi #
Dataset 2000 transaksi tidak kita tulis manual. Skrip seed menghasilkannya lewat pseudo-random number generator (PRNG) sederhana bernama mulberry32 — fungsi matematika yang mengubah satu angka seed menjadi urutan angka acak yang panjang. PRNG deterministik: seed yang sama selalu menghasilkan urutan yang sama, sehingga dataset yang dibangkitkan pun identik pada setiap eksekusi.
Urutan acak menentukan tanggal, kota, channel, judul buku, dan jumlah beli — artinya seluruh angka laporan di proyek ini (omzet total, peringkat produk, tren bulanan) bernilai tetap. Rentang tanggal juga dikunci pada jendela 1 Januari – 30 Juni 2026 alih-alih dihitung relatif terhadap waktu jalannya skrip.
Mengapa bukan Math.random()? Fungsi bawaan JavaScript tidak menerima seed — kita tidak punya cara mengembalikannya ke urutan angka tertentu. mulberry32 berperilaku seperti PRNG penuh dengan implementasi sepuluh baris, tanpa dependensi apa pun, dan distribusinya cukup baik untuk data demo. Alternatif populer sejenis antara lain xorshift dan splitmix32.
Skrip Seed #
// dataset-seed.js — dataset penjualan sintetis yang deterministik
// Jalankan lebih dulu (atau kapan pun untuk mereset ke kondisi awal):
// mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/dataset-seed.js
var ana = db.getSiblingDB("proyek_analitik")
// RNG deterministik: mulberry32 dengan seed tetap. Setiap kali skrip
// dijalankan, urutan angka acak yang dihasilkan selalu identik.
function mulberry32(seed) {
var t = seed >>> 0
return function () {
t += 0x6d2b79f5
var r = Math.imul(t ^ (t >>> 15), 1 | t)
r ^= r + Math.imul(r ^ (r >>> 7), 61 | r)
return ((r ^ (r >>> 14)) >>> 0) / 4294967296
}
}
var acak = mulberry32(20260101)
function pilih(daftar) {
return daftar[Math.floor(acak() * daftar.length)]
}
function rentang(min, maks) {
return min + Math.floor(acak() * (maks - min + 1))
}
// Katalog tetap: 12 judul buku pada 4 kategori, harga tidak berubah.
var PRODUK = [
{ judul: "Pemrograman Modern dengan Go", kategori: "pemrograman", harga: 148000 },
{ judul: "Rust untuk Sistem Andal", kategori: "pemrograman", harga: 165000 },
{ judul: "TypeScript dalam Praktik", kategori: "pemrograman", harga: 132000 },
{ judul: "MongoDB untuk Pengembang", kategori: "database", harga: 175000 },
{ judul: "Desain Skema Document", kategori: "database", harga: 150000 },
{ judul: "SQL dan Relational Dasar", kategori: "database", harga: 128000 },
{ judul: "Docker dari Dasar", kategori: "devops", harga: 138000 },
{ judul: "Kubernetes di Produksi", kategori: "devops", harga: 182000 },
{ judul: "Observabilitas Aplikasi", kategori: "devops", harga: 156000 },
{ judul: "Analisis Keuangan Praktis", kategori: "bisnis", harga: 121000 },
{ judul: "Manajemen Produk Digital", kategori: "bisnis", harga: 135000 },
{ judul: "Statistika untuk Keputusan", kategori: "bisnis", harga: 144000 },
]
var KOTA = ["Jakarta", "Bandung", "Surabaya", "Yogyakarta", "Medan"]
var CHANNEL = ["toko", "online", "aplikasi"]
// Rentang tanggal TETAP: 2026-01-01 s/d 2026-06-30 (181 hari).
// Tanggal relatif "sekarang" membuat angka laporan berubah antarjalankan,
// jadi proyek ini memakai jendela waktu yang dikunci.
var MULAI_MS = Date.UTC(2026, 0, 1)
var SATU_HARI = 24 * 60 * 60 * 1000
var JUMLAH_TRANSAKSI = 2000
function buatTransaksi() {
var offsetHari = rentang(0, 180)
var tanggal = new Date(
MULAI_MS + offsetHari * SATU_HARI +
rentang(8, 20) * 60 * 60 * 1000 + rentang(0, 59) * 60 * 1000
)
var jumlahItem = rentang(1, 3)
var items = []
var dipakai = {}
while (items.length < jumlahItem) {
var p = pilih(PRODUK)
if (dipakai[p.judul]) continue
dipakai[p.judul] = true
items.push({
produk: p.judul, kategori: p.kategori,
harga: p.harga, jumlah: rentang(1, 5),
})
}
var total = 0
items.forEach(function (it) { total += it.harga * it.jumlah })
return {
tanggal: tanggal, kota: pilih(KOTA), channel: pilih(CHANNEL),
items: items, total: total,
}
}
// Reset penuh lalu isi ulang: hasil selalu sama berapa kali pun dijalankan.
ana.dropDatabase()
var batch = []
for (var i = 0; i < JUMLAH_TRANSAKSI; i++) {
batch.push(buatTransaksi())
}
ana.transaksi.insertMany(batch)
// Statistik ringkas untuk memastikan dataset terisi sesuai rancangan.
print("== dataset proyek_analitik siap ==")
print("transaksi :", ana.transaksi.countDocuments({}))
var omzet = ana.transaksi.aggregate([
{ $group: { _id: null, total: { $sum: "$total" } } },
]).toArray()[0].total
print("omzet total :", omzet)
var rentangTanggal = ana.transaksi.aggregate([
{ $sort: { tanggal: 1 } },
{ $group: { _id: null, awal: { $first: "$tanggal" }, akhir: { $last: "$tanggal" } } },
]).toArray()[0]
print("tanggal awal :", rentangTanggal.awal.toISOString())
print("tanggal akhir :", rentangTanggal.akhir.toISOString())
var kategori = ana.transaksi.aggregate([
{ $unwind: "$items" },
{ $group: { _id: "$items.kategori" } },
{ $sort: { _id: 1 } },
]).toArray().map(function (k) { return k._id })
print("kategori :", kategori.join(", "))
print("contoh document:")
printjson(ana.transaksi.find({}, { _id: 0 }).sort({ _id: 1 }).limit(1).toArray()[0])Jalankan dari folder proyek:
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/dataset-seed.jsOutput yang Dihasilkan:
== dataset proyek_analitik siap ==
transaksi : 2000
omzet total : 1808214000
tanggal awal : 2026-01-01T10:25:00.000Z
tanggal akhir : 2026-06-30T19:36:00.000Z
kategori : bisnis, database, devops, pemrograman
contoh document:
{
tanggal: ISODate('2026-02-16T15:48:00.000Z'),
kota: 'Bandung',
channel: 'aplikasi',
items: [
{
produk: 'Observabilitas Aplikasi',
kategori: 'devops',
harga: 156000,
jumlah: 1
},
{
produk: 'Desain Skema Document',
kategori: 'database',
harga: 150000,
jumlah: 2
},
{
produk: 'Pemrograman Modern dengan Go',
kategori: 'pemrograman',
harga: 148000,
jumlah: 2
}
],
total: 752000
}Bedah Skrip Seed #
mulberry32(20260101)membangun generator dari seed 20260101. Setiap panggilan fungsi hasilnya menggeser state internalt, mengaduk bit lewatMath.imuldan geseran bitwise, lalu mengembalikan angka pecahan antara 0 dan 1. Seed yang sama menghasilkan urutan angka yang sama persis — inti reproduktibilitas proyek ini.pilih()danrentang()mengubah angka pecahan menjadi keputusan konkret:pilih(KOTA)memilih satu kota,rentang(1, 5)memilih jumlah beli antara 1 sampai 5. Seluruh variasi dataset mengalir dari kedua helper kecil ini.buatTransaksi()merangkai satu document: tanggal dibangun dari offset hari 0–180 ditambah jam 08.00–20.59, item diambil dari katalog tetap, dan objekdipakaimenjaga satu judul tidak muncul dua kali dalam transaksi yang sama. Fieldtotaldijumlahkan di akhir — nilai inilah yang nanti diuji suite tes terhadap isiitems.dropDatabase()sebeluminsertMany()membuat skrip idempoten: dijalankan sekali atau sepuluh kali, kondisi akhir database selalu sama. Perhatikan jugaprintjson()pada contoh document memakai projection{ _id: 0 }sehingga output bebas ObjectId yang nilainya berbeda antarjalankan.- Blok statistik menutup skrip dengan pemeriksaan mandiri: jumlah transaksi, omzet total lewat
$group+$sum, rentang tanggal lewat$sort+$first/$last, dan daftar kategori lewat$unwind+$group— tiga pola aggregation yang akan kita pakai penuh pada Bagian 2.
Memeriksa Kedalaman Dataset #
Statistik penutup skrip memandang data dari level transaksi. Satu pertanyaan analitik menuntut sudut pandang level item: berapa banyak baris belanja sesungguhnya, dan berapa unit buku yang berpindah tangan selama enam bulan?
use proyek_analitik
db.transaksi.aggregate([
{ $unwind: "$items" },
{ $group: {
_id: null,
barisItem: { $sum: 1 },
unitTerjual: { $sum: "$items.jumlah" },
} },
])Output yang Dihasilkan:
switched to db proyek_analitik
[
{ _id: null, barisItem: 4071, unitTerjual: 12224 }
]Stage $unwind membongkar setiap transaksi menjadi satu document per item, sehingga 2000 transaksi menghasilkan 4071 baris — rata-rata dua buku per transaksi. Dari baris-baris tersebut terjual 12.224 unit dengan omzet Rp1.808.214.000. Sebaran bulanannya juga sehat: antara 295 transaksi (Februari, bulan terpendek) sampai 362 transaksi (Januari) — tidak ada bulan yang kosong ataupun mendominasi. Keempat kategori terwakili, sehingga perbandingan antarkategori pada bagian-bagian selanjutnya selalu punya bahan.
Apa Berikutnya #
Dataset siap dan angka-angkanya terkunci. Bagian 2 menyusun tiga pipeline laporan inti di atasnya: omzet per kategori dan per bulan dengan $dateTrunc, peringkat produk dengan $unwind dan $group, serta tren bulanan dengan window function $setWindowFields.