Total penjualan berjalan, rata-rata tiga transaksi terakhir, peringkat buku dalam kategorinya — semua hitungan ini butuh konteks document tetangga. $group tidak bisa menolong: melipat banyak document menjadi satu, padahal kita ingin setiap document tetap utuh sambil membawa nilai hasil hitungan tetangganya. Sejak MongoDB 5.0, kebutuhan itu dijawab stage $setWindowFields — window functions ala SQL yang menghitung atas “jendela” document di sekitar posisi saat ini. Chapter ini membangun peringkat penjualan, running total, moving average, dan total rentang waktu dari transaksi toko buku.
Peta materi Chapter ini:
- Anatomi
$setWindowFields:partitionBy,sortBy,output,window. - Keluarga peringkat:
$rank,$denseRank,$documentNumber. - Window berbasis posisi (
documents) untuk running total dan moving average. - Window berbasis nilai (
range) untuk total rentang waktu.
Anatomi $setWindowFields #
Empat komponennya:
| Komponen | Wajib | Peran |
|---|---|---|
partitionBy | tidak | field/ekspresi pembagi partisi; hitungan berlangsung di dalam partisi |
sortBy | ya | urutan document dalam partisi — posisi jendela mengacu ke urutan ini |
output | ya | field hasil + operator window ($sum, $avg, $rank, dan lainnya) |
window | tidak | batas jendela; tanpa window, rank menghitung seluruh partisi |
Siapkan data uji — katalog buku plus riwayat transaksi yang urut waktunya:
use latihan_agregasi2
db.dropDatabase()
db.buku.insertMany([
{ judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "fiksi", terjual: 120, tanggal_terbit: ISODate("2017-02-01T00:00:00Z"), tag: ["fiksi", "sastra", "penghargaan"], ulasan: [ { pengulas: "Budi", bintang: 5 }, { pengulas: "Siti", bintang: 4 } ] },
{ judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "fiksi", terjual: 250, tanggal_terbit: ISODate("2014-09-02T00:00:00Z"), tag: ["fiksi", "remaja", "fantasi"], ulasan: [ { pengulas: "Dewi", bintang: 5 }, { pengulas: "Budi", bintang: 4 }, { pengulas: "Siti", bintang: 5 } ] },
{ judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "nonfiksi", terjual: 180, tanggal_terbit: ISODate("2018-07-01T00:00:00Z"), tag: ["filsafat", "pengembangan diri"], ulasan: [ { pengulas: "Siti", bintang: 5 } ] },
{ judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi", terjual: 310, tanggal_terbit: ISODate("2018-10-16T00:00:00Z"), tag: ["pengembangan diri", "terjemahan"], ulasan: [ { pengulas: "Budi", bintang: 5 }, { pengulas: "Dewi", bintang: 4 } ] },
{ judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi", terjual: 120, tanggal_terbit: ISODate("2016-06-15T00:00:00Z"), tag: ["fiksi", "remaja"], ulasan: [ { pengulas: "Dewi", bintang: 4 } ] },
{ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", harga: 105000, kategori: "sastra", terjual: 150, tanggal_terbit: ISODate("1980-05-25T00:00:00Z"), tag: ["sastra", "sejarah"], ulasan: [ { pengulas: "Budi", bintang: 5 }, { pengulas: "Siti", bintang: 5 }, { pengulas: "Dewi", bintang: 4 } ] },
{ judul: "Hujan Bulan Juni", penulis: "Sapardi Djoko Damono", harga: 65000, kategori: "puisi", terjual: 75, tanggal_terbit: ISODate("1994-06-10T00:00:00Z"), tag: ["puisi", "sastra"] },
{ judul: "Ensiklopedia Sains Populer", penulis: "Tim Sains", harga: 250000, kategori: "referensi", terjual: 40, tanggal_terbit: ISODate("2020-03-05T00:00:00Z"), tag: ["referensi", "sains"], ulasan: [ { pengulas: "Dewi", bintang: 4 } ] }
])
db.transaksi.insertMany([
{ tanggal: ISODate("2026-11-28T10:20:00Z"), buku_judul: "Bumi", jumlah: 2 },
{ tanggal: ISODate("2026-11-29T14:45:00Z"), buku_judul: "Atomic Habits", jumlah: 3 },
{ tanggal: ISODate("2026-11-30T16:10:00Z"), buku_judul: "Filosofi Teras", jumlah: 1 },
{ tanggal: ISODate("2026-12-01T09:15:00Z"), buku_judul: "Bumi", jumlah: 2 },
{ tanggal: ISODate("2026-12-01T11:30:00Z"), buku_judul: "Atomic Habits", jumlah: 1 },
{ tanggal: ISODate("2026-12-01T15:45:00Z"), buku_judul: "Bumi", jumlah: 3 },
{ tanggal: ISODate("2026-12-02T10:05:00Z"), buku_judul: "Laut Bercerita", jumlah: 1 },
{ tanggal: ISODate("2026-12-02T13:20:00Z"), buku_judul: "Filosofi Teras", jumlah: 4 },
{ tanggal: ISODate("2026-12-02T16:40:00Z"), buku_judul: "Bumi", jumlah: 2 },
{ tanggal: ISODate("2026-12-03T09:50:00Z"), buku_judul: "Atomic Habits", jumlah: 2 },
{ tanggal: ISODate("2026-12-03T14:10:00Z"), buku_judul: "Bumi Manusia", jumlah: 1 },
{ tanggal: ISODate("2026-12-03T17:25:00Z"), buku_judul: "Bumi", jumlah: 1 },
{ tanggal: ISODate("2026-12-04T10:30:00Z"), buku_judul: "Rumah Kaca", jumlah: 2 },
{ tanggal: ISODate("2026-12-04T13:55:00Z"), buku_judul: "Bumi", jumlah: 2 },
{ tanggal: ISODate("2026-12-04T18:05:00Z"), buku_judul: "Hujan Bulan Juni", jumlah: 3 }
])Output yang Dihasilkan:
switched to db latihan_agregasi2
{ ok: 1, dropped: 'latihan_agregasi2' }
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3a795034d406899f7b262'),
'1': ObjectId('6ad3a795034d406899f7b263'),
'2': ObjectId('6ad3a795034d406899f7b264'),
'3': ObjectId('6ad3a795034d406899f7b265'),
'4': ObjectId('6ad3a795034d406899f7b266'),
'5': ObjectId('6ad3a795034d406899f7b267'),
'6': ObjectId('6ad3a795034d406899f7b268'),
'7': ObjectId('6ad3a795034d406899f7b269')
}
}
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3a795034d406899f7b26a'),
'1': ObjectId('6ad3a795034d406899f7b26b'),
'2': ObjectId('6ad3a795034d406899f7b26c'),
'3': ObjectId('6ad3a795034d406899f7b26d'),
'4': ObjectId('6ad3a795034d406899f7b26e'),
'5': ObjectId('6ad3a795034d406899f7b26f'),
'6': ObjectId('6ad3a795034d406899f7b270'),
'7': ObjectId('6ad3a795034d406899f7b271'),
'8': ObjectId('6ad3a795034d406899f7b272'),
'9': ObjectId('6ad3a795034d406899f7b273'),
'10': ObjectId('6ad3a795034d406899f7b274'),
'11': ObjectId('6ad3a795034d406899f7b275'),
'12': ObjectId('6ad3a795034d406899f7b276'),
'13': ObjectId('6ad3a795034d406899f7b277'),
'14': ObjectId('6ad3a795034d406899f7b278')
}
}Peringkat: $rank, $denseRank, $documentNumber #
Peringkat penjualan seluruh toko — tanpa partitionBy, seluruh collection menjadi satu partisi:
db.buku.aggregate([
{ $setWindowFields: {
sortBy: { terjual: -1 },
output: {
peringkat: { $rank: {} },
peringkatPadat: { $denseRank: {} },
nomorUrut: { $documentNumber: {} }
}
} },
{ $project: { _id: 0, judul: 1, terjual: 1, peringkat: 1, peringkatPadat: 1, nomorUrut: 1 } }
])Output yang Dihasilkan:
[
{
judul: 'Atomic Habits',
terjual: 310,
peringkat: 1,
peringkatPadat: 1,
nomorUrut: 1
},
{
judul: 'Bumi',
terjual: 250,
peringkat: 2,
peringkatPadat: 2,
nomorUrut: 2
},
{
judul: 'Filosofi Teras',
terjual: 180,
peringkat: 3,
peringkatPadat: 3,
nomorUrut: 3
},
{
judul: 'Bumi Manusia',
terjual: 150,
peringkat: 4,
peringkatPadat: 4,
nomorUrut: 4
},
{
judul: 'Laut Bercerita',
terjual: 120,
peringkat: 5,
peringkatPadat: 5,
nomorUrut: 5
},
{
judul: 'Rumah Kaca',
terjual: 120,
peringkat: 5,
peringkatPadat: 5,
nomorUrut: 6
},
{
judul: 'Hujan Bulan Juni',
terjual: 75,
peringkat: 7,
peringkatPadat: 6,
nomorUrut: 7
},
{
judul: 'Ensiklopedia Sains Populer',
terjual: 40,
peringkat: 8,
peringkatPadat: 7,
nomorUrut: 8
}
]Tiga kolom itu tampak mirip sampai Laut Bercerita dan Rumah Kaca — keduanya terjual 120 — berbagi posisi. $rank memberi keduanya peringkat 5, lalu melompat ke 7 untuk buku berikutnya, persis perilaku RANK() SQL. $denseRank juga seri di 5 tetapi lanjut ke 6 tanpa lompatan. $documentNumber mengabaikan seri dan menomori posisi fisik 1–8. Tambahkan partitionBy: "$kategori", dan perhitungan yang sama berlangsung terpisah di tiap kategori — peringkat satu tidak lagi hanya milik satu buku di seluruh toko.
Running Total dengan Window documents #
Window documents mengukur jarak posisi: ["unbounded", "current"] berarti “dari document pertama sampai document saat ini”. Inilah resep total berjalan — kita batasi tampilannya ke transaksi Bumi:
db.transaksi.aggregate([
{ $setWindowFields: {
partitionBy: "$buku_judul",
sortBy: { tanggal: 1 },
output: {
totalBerjalan: { $sum: "$jumlah", window: { documents: ["unbounded", "current"] } }
}
} },
{ $match: { buku_judul: "Bumi" } },
{ $project: { _id: 0, tanggal: 1, jumlah: 1, totalBerjalan: 1 } }
])Output yang Dihasilkan:
[
{
tanggal: ISODate('2026-11-28T10:20:00.000Z'),
jumlah: 2,
totalBerjalan: 2
},
{
tanggal: ISODate('2026-12-01T09:15:00.000Z'),
jumlah: 2,
totalBerjalan: 4
},
{
tanggal: ISODate('2026-12-01T15:45:00.000Z'),
jumlah: 3,
totalBerjalan: 7
},
{
tanggal: ISODate('2026-12-02T16:40:00.000Z'),
jumlah: 2,
totalBerjalan: 9
},
{
tanggal: ISODate('2026-12-03T17:25:00.000Z'),
jumlah: 1,
totalBerjalan: 10
},
{
tanggal: ISODate('2026-12-04T13:55:00.000Z'),
jumlah: 2,
totalBerjalan: 12
}
]partitionBy: "$buku_judul" membuat setiap buku punya lintasan sendiri; enam transaksi Bumi (2, 2, 3, 2, 1, 2) menghasilkan total berjalan 2, 4, 7, 9, 10, 12 — angka terakhir persis total penjualan Bumi tanpa $group yang melipat document. Dengan $match diletakkan setelah $setWindowFields, perhitungan tetap jujur untuk semua buku; geser $match ke depan bila hanya satu buku yang memang dibutuhkan.
Moving Average dan Pembulatannya #
Jendela [-2, 0] berarti “dua document sebelum yang saat ini, sampai yang saat ini” — tiga transaksi terakhir. Rata-ratanya:
db.transaksi.aggregate([
{ $setWindowFields: {
sortBy: { tanggal: 1 },
output: { rataTigaTransaksi: { $avg: "$jumlah", window: { documents: [-2, 0] } } }
} },
{ $set: { rataTigaTransaksi: { $round: ["$rataTigaTransaksi", 2] } } },
{ $match: { tanggal: { $gte: ISODate("2026-12-04T00:00:00Z") } } },
{ $project: { _id: 0, tanggal: 1, jumlah: 1, rataTigaTransaksi: 1 } }
])Output yang Dihasilkan:
[
{
tanggal: ISODate('2026-12-04T10:30:00.000Z'),
jumlah: 2,
rataTigaTransaksi: 1.33
},
{
tanggal: ISODate('2026-12-04T13:55:00.000Z'),
jumlah: 2,
rataTigaTransaksi: 1.67
},
{
tanggal: ISODate('2026-12-04T18:05:00.000Z'),
jumlah: 3,
rataTigaTransaksi: 2.33
}
]Pada 18:05, rata-rata tiga transaksi terakhir (2, 2, 3) tercatat 2,33. Perhatikan struktur pentingnya: pembulatan $round dikerjakan oleh stage $set terpisah setelah $setWindowFields. output hanya menerima operator window — membungkus $avg dengan $round langsung di dalam output ditolak server dengan pesan Unrecognized window function, $round. Hitung dulu di window, rapikan bentuknya di stage berikutnya.
Window range: Total Tiga Hari Terakhir #
Window range mengukur jarak nilai dari field sortBy — dengan unit, jaraknya berbasis waktu. range: [-3, 0] berarti “document yang waktunya paling lama 3 hari sebelum document saat ini”:
db.transaksi.aggregate([
{ $setWindowFields: {
sortBy: { tanggal: 1 },
output: { totalTigaHari: { $sum: "$jumlah", window: { range: [-3, 0], unit: "day" } } }
} },
{ $match: { tanggal: { $gte: ISODate("2026-12-03T00:00:00Z") } } },
{ $project: { _id: 0, tanggal: 1, jumlah: 1, totalTigaHari: 1 } }
])Output yang Dihasilkan:
[
{
tanggal: ISODate('2026-12-03T09:50:00.000Z'),
jumlah: 2,
totalTigaHari: 16
},
{
tanggal: ISODate('2026-12-03T14:10:00.000Z'),
jumlah: 1,
totalTigaHari: 17
},
{
tanggal: ISODate('2026-12-03T17:25:00.000Z'),
jumlah: 1,
totalTigaHari: 17
},
{
tanggal: ISODate('2026-12-04T10:30:00.000Z'),
jumlah: 2,
totalTigaHari: 17
},
{
tanggal: ISODate('2026-12-04T13:55:00.000Z'),
jumlah: 2,
totalTigaHari: 18
},
{
tanggal: ISODate('2026-12-04T18:05:00.000Z'),
jumlah: 3,
totalTigaHari: 18
}
]Pada transaksi 13:55 tanggal 4 Desember, jendela menampung semua transaksi dari 13:55 tanggal 1 Desember hingga saat itu — sembilan transaksi dengan total 18. Bandingkan dengan documents: [-2, 0] yang selalu tepat tiga document: range menjawab “tiga hari terakhir”, documents menjawab “tiga transaksi terakhir”. Untuk data deret waktu, pilihan di antara keduanya menentukan makna angka yang dilaporkan.
Rangkuman #
$setWindowFieldsmenghitung lintas document tetangga tanpa melipatnya — setiap document kelihatan utuh membawa nilai window.partitionBymembagi partisi,sortBymenentukan urutan di dalamnya; keduanya menentukan siapa “tetangga” sebuah document.$rankmelompat setelah seri,$denseRankpadat tanpa lompatan,$documentNumbermenomori posisi fisik.window: { documents: [...] }berbasis posisi (running total, moving average);window: { range: [...], unit: ... }berbasis nilai/waktu (total N hari terakhir).- Operator window tidak boleh dibungkus operator biasa — pisahkan pembulatan ke
$setdi stage berikutnya.
Hasil perhitungan selama ini hanya hidup sesaat di layar. Bagaimana bila laporan harian itu perlu disimpan agar dashboard membacanya tanpa menghitung ulang? $merge dan $out, stage penulisan hasil, menunggu di Chapter berikutnya.