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:

  • $out dalam bentuk string dan bentuk object { db, coll }.
  • $merge dengan whenMatched dan whenNotMatched.
  • Pola laporan harian: ETL ulang dengan $merge, termasuk whenMatched berbentuk 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:

BentukPerilaku
{ $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:

js
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:

text
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:

js
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:

text
3

1

Perhatikan 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:

js
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:

text
{ judul: 'Bumi Manusia', kategori: 'sastra' }

$merge: Gabungkan tanpa Menghapus #

$merge menulis hasil pipeline ke collection target dengan logika upsert per document. Field-fieldnya:

FieldPilihanPerilaku
into"nama" atau { db, coll }collection target
onfield kuncidefault _id; field lain wajib punya unique index
whenMatchedmerge (bawaan), replace, keepExisting, fail, atau pipelinekunci sudah ada di target
whenNotMatchedinsert (bawaan), discard, failkunci belum ada di target

Pola laporan harian klasiknya — kelompokkan transaksi per hari, lalu tuangkan ke laporan_harian:

js
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:

text
[
  {
    _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:

js
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:

text
{
  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:

js
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:

text
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:

js
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:

text
{
  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:

js
db.getSiblingDB("latihan_agregasi_arsip").dropDatabase()

Output yang Dihasilkan:

text
{ ok: 1, dropped: 'latihan_agregasi_arsip' }

Rangkuman #

  • $out mengganti collection target sepenuhnya dengan hasil pipeline; cocok untuk snapshot yang selalu dihitung ulang dari nol.
  • $merge melakukan upsert per baris: whenMatched (merge/replace/keepExisting/fail/pipeline) dan whenNotMatched (insert/discard/fail).
  • Pola laporan harian: $group per $dateTrunc hari + $merge — jalankan ulang kapan pun; baris lama diperbarui, hari baru disisipkan.
  • whenMatched berbentuk pipeline menggabungkan document lama ($ROOT) dengan yang masuk ($$new) — dasar ETL inkremental.
  • Field on selain _id menuntut 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.