Pipeline aggregation secara bawaan mengirim hasilnya ke layar — sesaat tampil, lalu hilang. Dashboard yang membaca jutaan transaksi tidak boleh menghitung ulang agregasi itu di setiap request; laporan seperti itu perlu dihitung sekali lalu disimpan, sehingga pembaca cukup membaca hasilnya. Dua stage penulisan menutup celah ini: $out mengganti sebuah collection sepenuhnya dengan hasil pipeline, sedangkan $merge menggabungkan hasil ke collection yang sudah ada — naik atau perbarui, baris demi baris. Chapter ini mematerialisasikan laporan penjualan harian toko buku dengan keduanya.
Peta materi Chapter ini:
$outdalam bentuk string dan bentuk object{ db, coll }.$mergedenganwhenMatcheddanwhenNotMatched.- Pola laporan harian: ETL ulang dengan
$merge, termasukwhenMatchedberbentuk pipeline.
$out: Ganti Collection Sepenuhnya #
$out berada di ujung pipeline; collection target diganti total dengan hasil pipeline — document lama terhapus, index collection ikut dibangun ulang. Bentuknya dua macam:
| Bentuk | Perilaku |
|---|---|
{ $out: "nama" } | tulis ke collection di database yang sedang aktif |
{ $out: { db: "...", coll: "..." } } | tulis ke collection di database lain |
Siapkan data uji — katalog buku plus transaksi:
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('6ad3a84da1faf538e86620f8'),
'1': ObjectId('6ad3a84da1faf538e86620f9'),
'2': ObjectId('6ad3a84da1faf538e86620fa'),
'3': ObjectId('6ad3a84da1faf538e86620fb'),
'4': ObjectId('6ad3a84da1faf538e86620fc'),
'5': ObjectId('6ad3a84da1faf538e86620fd'),
'6': ObjectId('6ad3a84da1faf538e86620fe'),
'7': ObjectId('6ad3a84da1faf538e86620ff')
}
}
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3a84ea1faf538e8662100'),
'1': ObjectId('6ad3a84ea1faf538e8662101'),
'2': ObjectId('6ad3a84ea1faf538e8662102'),
'3': ObjectId('6ad3a84ea1faf538e8662103'),
'4': ObjectId('6ad3a84ea1faf538e8662104'),
'5': ObjectId('6ad3a84ea1faf538e8662105'),
'6': ObjectId('6ad3a84ea1faf538e8662106'),
'7': ObjectId('6ad3a84ea1faf538e8662107'),
'8': ObjectId('6ad3a84ea1faf538e8662108'),
'9': ObjectId('6ad3a84ea1faf538e8662109'),
'10': ObjectId('6ad3a84ea1faf538e866210a'),
'11': ObjectId('6ad3a84ea1faf538e866210b'),
'12': ObjectId('6ad3a84ea1faf538e866210c'),
'13': ObjectId('6ad3a84ea1faf538e866210d'),
'14': ObjectId('6ad3a84ea1faf538e866210e')
}
}Ekspor buku fiksi ke collection baru, lalu ulangi dengan buku puisi:
db.buku.aggregate([ { $match: { kategori: "fiksi" } }, { $out: "buku_fiksi" } ])
db.buku_fiksi.countDocuments()
db.buku.aggregate([ { $match: { kategori: "puisi" } }, { $out: "buku_fiksi" } ])
db.buku_fiksi.countDocuments()Output yang Dihasilkan:
3
1Perhatikan perubahan hitungan: tiga buku fiksi tersimpan, lalu pipeline kedua mengganti seluruh isi buku_fiksi dengan satu buku puisi — bukan menambah. Karena itulah $out cocok untuk hasil yang selalu dihitung dari nol (snapshot penuh), dan berbahaya bila collection target menyimpan data lain. Bentuk object-nya menunjuk database berbeda:
db.buku.aggregate([ { $match: { kategori: "sastra" } }, { $out: { db: "latihan_agregasi_arsip", coll: "buku_sastra" } } ])
db.getSiblingDB("latihan_agregasi_arsip").buku_sastra.findOne({}, { _id: 0, judul: 1, kategori: 1 })Output yang Dihasilkan:
{ judul: 'Bumi Manusia', kategori: 'sastra' }$merge: Gabungkan tanpa Menghapus #
$merge menulis hasil pipeline ke collection target dengan logika upsert per document. Field-fieldnya:
| Field | Pilihan | Perilaku |
|---|---|---|
into | "nama" atau { db, coll } | collection target |
on | field kunci | default _id; field lain wajib punya unique index |
whenMatched | merge (bawaan), replace, keepExisting, fail, atau pipeline | kunci sudah ada di target |
whenNotMatched | insert (bawaan), discard, fail | kunci belum ada di target |
Pola laporan harian klasiknya — kelompokkan transaksi per hari, lalu tuangkan ke laporan_harian:
db.transaksi.aggregate([
{ $group: { _id: { $dateTrunc: { date: "$tanggal", unit: "day" } }, totalJumlah: { $sum: "$jumlah" }, jumlahTransaksi: { $sum: 1 } } },
{ $merge: { into: "laporan_harian" } }
])
db.laporan_harian.find({}, { _id: 1, totalJumlah: 1, jumlahTransaksi: 1 }).sort({ _id: 1 })Output yang Dihasilkan:
[
{
_id: ISODate('2026-11-28T00:00:00.000Z'),
totalJumlah: 2,
jumlahTransaksi: 1
},
{
_id: ISODate('2026-11-29T00:00:00.000Z'),
totalJumlah: 3,
jumlahTransaksi: 1
},
{
_id: ISODate('2026-11-30T00:00:00.000Z'),
totalJumlah: 1,
jumlahTransaksi: 1
},
{
_id: ISODate('2026-12-01T00:00:00.000Z'),
totalJumlah: 6,
jumlahTransaksi: 3
},
{
_id: ISODate('2026-12-02T00:00:00.000Z'),
totalJumlah: 7,
jumlahTransaksi: 3
},
{
_id: ISODate('2026-12-03T00:00:00.000Z'),
totalJumlah: 4,
jumlahTransaksi: 3
},
{
_id: ISODate('2026-12-04T00:00:00.000Z'),
totalJumlah: 7,
jumlahTransaksi: 3
}
]Tujuh hari laporan tersimpan — _id grup hasil $dateTrunc menjadi kunci on bawaan. Sekarang uji perilaku upsert-nya: tiga transaksi baru masuk, satu koreksi untuk 4 Desember dan dua transaksi 5 Desember, lalu pipeline yang sama dijalankan ulang hanya untuk data itu:
db.transaksi.insertMany([
{ tanggal: ISODate("2026-12-04T19:30:00Z"), buku_judul: "Bumi", jumlah: 2 },
{ tanggal: ISODate("2026-12-05T09:40:00Z"), buku_judul: "Bumi", jumlah: 3 },
{ tanggal: ISODate("2026-12-05T14:20:00Z"), buku_judul: "Hujan Bulan Juni", jumlah: 1 }
])
db.transaksi.aggregate([
{ $match: { tanggal: { $gte: ISODate("2026-12-04T00:00:00Z") } } },
{ $group: { _id: { $dateTrunc: { date: "$tanggal", unit: "day" } }, totalJumlah: { $sum: "$jumlah" }, jumlahTransaksi: { $sum: 1 } } },
{ $merge: { into: "laporan_harian" } }
])
db.laporan_harian.find({}, { _id: 1, totalJumlah: 1, jumlahTransaksi: 1 }).sort({ _id: 1 })Output yang Dihasilkan:
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3a84ea1faf538e866210f'),
'1': ObjectId('6ad3a84ea1faf538e8662110'),
'2': ObjectId('6ad3a84ea1faf538e8662111')
}
}
[
{
_id: ISODate('2026-11-28T00:00:00.000Z'),
totalJumlah: 2,
jumlahTransaksi: 1
},
{
_id: ISODate('2026-11-29T00:00:00.000Z'),
totalJumlah: 3,
jumlahTransaksi: 1
},
{
_id: ISODate('2026-11-30T00:00:00.000Z'),
totalJumlah: 1,
jumlahTransaksi: 1
},
{
_id: ISODate('2026-12-01T00:00:00.000Z'),
totalJumlah: 6,
jumlahTransaksi: 3
},
{
_id: ISODate('2026-12-02T00:00:00.000Z'),
totalJumlah: 7,
jumlahTransaksi: 3
},
{
_id: ISODate('2026-12-03T00:00:00.000Z'),
totalJumlah: 4,
jumlahTransaksi: 3
},
{
_id: ISODate('2026-12-04T00:00:00.000Z'),
totalJumlah: 9,
jumlahTransaksi: 4
},
{
_id: ISODate('2026-12-05T00:00:00.000Z'),
totalJumlah: 4,
jumlahTransaksi: 2
}
]Dua hal terjadi sekaligus: baris 4 Desember diperbarui menjadi 9 buku dan 4 transaksi (whenMatched: "merge" menggabungkan field hasil ke document lama), sementara 5 Desember disisipkan sebagai baris baru (whenNotMatched: "insert"). Enam baris lain tidak tersentuh — inilah beda fundamentalnya dengan $out yang selalu mengganti semuanya.
whenMatched Berbentuk Pipeline #
Pilihan paling kuat dari whenMatched adalah pipeline kecil. Di dalamnya, $$ROOT merujuk document lama di target dan $$new merujuk hasil pipeline yang masuk — kita bebas menggabungkan keduanya. Bangun ulang laporan dari nol untuk data sampai 2 Desember:
db.laporan_harian.drop()
db.transaksi.aggregate([
{ $match: { tanggal: { $lt: ISODate("2026-12-03T00:00:00Z") } } },
{ $group: { _id: { $dateTrunc: { date: "$tanggal", unit: "day" } }, totalJumlah: { $sum: "$jumlah" }, jumlahTransaksi: { $sum: 1 } } },
{ $merge: { into: "laporan_harian" } }
])
db.laporan_harian.find({ _id: ISODate("2026-12-02T00:00:00Z") }, { _id: 1, totalJumlah: 1, jumlahTransaksi: 1 })Output yang Dihasilkan:
true
[
{
_id: ISODate('2026-12-02T00:00:00.000Z'),
totalJumlah: 7,
jumlahTransaksi: 3
}
]Baris 2 Desember tercatat 7 buku dari 3 transaksi. Kemudian dua transaksi malam itu datang terlambat — batchnya hanya berisi delta tersebut, dan whenMatched berbentuk pipeline menambahkan delta ke nilai lama:
db.transaksi.insertMany([
{ tanggal: ISODate("2026-12-02T20:15:00Z"), buku_judul: "Atomic Habits", jumlah: 2 },
{ tanggal: ISODate("2026-12-02T21:40:00Z"), buku_judul: "Bumi", jumlah: 1 }
])
db.transaksi.aggregate([
{ $match: { tanggal: { $gte: ISODate("2026-12-02T20:00:00Z"), $lt: ISODate("2026-12-03T00:00:00Z") } } },
{ $group: { _id: { $dateTrunc: { date: "$tanggal", unit: "day" } }, totalJumlah: { $sum: "$jumlah" }, jumlahTransaksi: { $sum: 1 } } },
{ $merge: { into: "laporan_harian",
whenMatched: [ { $set: {
totalJumlah: { $add: ["$totalJumlah", "$$new.totalJumlah"] },
jumlahTransaksi: { $add: ["$jumlahTransaksi", "$$new.jumlahTransaksi"] } } } ],
whenNotMatched: "insert" } }
])
db.laporan_harian.find({ _id: ISODate("2026-12-02T00:00:00Z") }, { _id: 1, totalJumlah: 1, jumlahTransaksi: 1 })Output yang Dihasilkan:
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3a84ea1faf538e8662112'),
'1': ObjectId('6ad3a84ea1faf538e8662113')
}
}
[
{
_id: ISODate('2026-12-02T00:00:00.000Z'),
totalJumlah: 10,
jumlahTransaksi: 5
}
]Baris 2 Desember naik dari 7/3 menjadi 10/5 — pipeline $set menjumlahkan field lama ($totalJumlah) dengan field dari batch baru ($$new.totalJumlah). Inilah pola ETL inkremental: job berkala membawa hanya data baru, laporan kumulatif diperbarui tanpa membaca ulang seluruh riwayat transaksi.
Sebagai penutup, bersihkan database arsip yang kita buat untuk demo bentuk object $out:
db.getSiblingDB("latihan_agregasi_arsip").dropDatabase()Output yang Dihasilkan:
{ ok: 1, dropped: 'latihan_agregasi_arsip' }Rangkuman #
$outmengganti collection target sepenuhnya dengan hasil pipeline; cocok untuk snapshot yang selalu dihitung ulang dari nol.$mergemelakukan upsert per baris:whenMatched(merge/replace/keepExisting/fail/pipeline) danwhenNotMatched(insert/discard/fail).- Pola laporan harian:
$groupper$dateTrunchari +$merge— jalankan ulang kapan pun; baris lama diperbarui, hari baru disisipkan. whenMatchedberbentuk pipeline menggabungkan document lama ($ROOT) dengan yang masuk ($$new) — dasar ETL inkremental.- Field
onselain_idmenuntut unique index di collection target.
Laporan tersimpan, dashboard ringan. Pertanyaan terakhir Section ini: bagaimana memastikan pipeline itu sendiri berjalan seefisien mungkin? Chapter Performa Aggregation menutup dengan urutan tahap, index, memori, dan alat diagnosisnya.