Collection buku di toko kita menyimpan stok, harga, kategori, dan catatan penjualan bulanan. Seiring data bertumbuh, pertanyaan yang diajukan manajemen tidak lagi berbentuk “document mana yang cocok”, melainkan “berapa total stok sekarang”, “berapa rata-rata harga per kategori”, dan “tag apa yang paling sering dipakai”. find() dirancang untuk menemukan document, bukan untuk menghitung atau mengubah bentuk hasil. Aggregation adalah kerangka kerja MongoDB yang menjawab kelompok pertanyaan kedua — dan Section ini membangunnya tahap demi tahap.

Peta materi Chapter ini:

  • Batas find(): mengapa query biasa tidak bisa menghitung.
  • Tiga jalur agregasi: single-purpose method, aggregation pipeline, dan mapReduce.
  • Alasan aggregation pipeline menjadi pilihan utama.

Batas find(): Menemukan, Bukan Menghitung #

Query operator yang kita kuasai sejauh ini ($gte, $in, $elemMatch, dan kawan-kawannya) memiliki satu kosakata: mencocokkan. Tidak ada operator yang berarti “jumlahkan field stok dari semua document yang cocok”. Tanpa aggregation, satu-satunya jalan adalah menarik semua document ke sisi aplikasi lalu menghitung sendiri. Kita siapkan data uji dulu — collection buku ini akan menemani kita sepanjang Section Aggregation:

js
use latihan_agregasi
db.dropDatabase()

Output yang Dihasilkan:

text
switched to db latihan_agregasi
{ ok: 1, dropped: 'latihan_agregasi' }
js
db.buku.insertMany([
  { _id: 1, judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "Fiksi", tag: ["sastra", "novel"], stok: 12, tahun_terbit: 2017, terjual: [ { bulan: "Januari", jumlah: 14 }, { bulan: "Februari", jumlah: 9 }, { bulan: "Maret", jumlah: 11 } ] },
  { _id: 2, judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "Fiksi", tag: ["novel", "fantasi"], stok: 20, tahun_terbit: 2014, terjual: [ { bulan: "Januari", jumlah: 21 }, { bulan: "Februari", jumlah: 17 }, { bulan: "Maret", jumlah: 25 } ] },
  { _id: 3, judul: "Supernova: Ksatria, Puteri, dan Bintang Jatuh", penulis: "Dee Lestari", harga: 95000, kategori: "Fiksi", tag: ["novel", "sains"], stok: 6, tahun_terbit: 2001, terjual: [ { bulan: "Januari", jumlah: 7 }, { bulan: "Februari", jumlah: 5 } ] },
  { _id: 4, judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "Nonfiksi", tag: ["filsafat", "pengembangan diri"], stok: 8, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 12 }, { bulan: "Februari", jumlah: 15 }, { bulan: "Maret", jumlah: 10 } ] },
  { _id: 5, judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "Nonfiksi", tag: ["pengembangan diri", "kebiasaan"], stok: 15, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 18 }, { bulan: "Februari", jumlah: 22 }, { bulan: "Maret", jumlah: 16 } ] },
  { _id: 6, judul: "Kamus Bahasa Indonesia", penulis: "Badan Bahasa", harga: 55000, kategori: "Referensi", tag: ["referensi", "bahasa"], stok: 30, tahun_terbit: 2016, terjual: [] }
])

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedIds: { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
}

Enam judul buku, tiga kategori, dan satu catatan penting: Kamus Bahasa Indonesia memiliki array terjual kosong. Sekarang kita coba menjawab “berapa total stok seluruh buku?” dengan cara lama — mengambil semua document lalu menjumlahkan di sisi klien:

js
let totalStok = 0
db.buku.find({}, { stok: 1, _id: 0 }).forEach(buku => totalStok += buku.stok)
totalStok

Output yang Dihasilkan:

text
91

Angka 91 benar, tetapi mekanismenya boros: find() mengirim keenam document melewati jaringan ke mongosh, lalu JavaScript yang menjumlahkan. Bayangkan collection berisi satu juta document — seluruh beban data berpindah hanya untuk menghasilkan satu angka. Bandingkan dengan cara aggregation:

js
db.buku.aggregate([ { $group: { _id: null, totalStok: { $sum: "$stok" } } } ])

Output yang Dihasilkan:

text
[ { _id: null, totalStok: 91 } ]

Satu perintah, hasil yang sama, dan seluruh perhitungan terjadi di server. Mari kita bedah bentuknya secara singkat:

  1. db.buku.aggregate([...]): menjalankan aggregation pipeline pada collection buku; argumennya adalah daftar tahap pengolahan.
  2. { $group: ... }: tahap yang mengelompokkan document; _id: null berarti semua document masuk satu kelompok.
  3. totalStok: { $sum: "$stok" }: akumulator yang menjumlahkan field stok dari tiap anggota kelompok.

Jangan khawatir kalau detail sintaksnya belum jelas — dua Chapter berikutnya membedah pipeline ini sampai ke detail terkecil. Yang penting sekarang: menghitung adalah pekerjaan server.

Tiga Jalur Agregasi #

MongoDB menyediakan tiga jalur untuk mengagregasi data. Dua di antaranya sudah pernah kita pakai tanpa kita sadari:

JalurBentukCocok untuk
Single-purpose aggregationcountDocuments(), distinct(), estimatedDocumentCount()Satu jawaban spesifik: jumlah atau daftar nilai unik
Aggregation pipelineaggregate([ ... ])Perhitungan berlapis, pengelompokan, transformasi, join
mapReducedb.buku.mapReduce(...)Warisan era lama; deprecated, hindari untuk kode baru

Method single-purpose adalah pembungkus tipis di atas konsep agregasi. Kita buktikan keduanya masih bekerja seperti yang kita kenal dari Section Operasi CRUD:

js
db.buku.countDocuments({ kategori: "Fiksi" })
db.buku.distinct("kategori")
db.buku.countDocuments({})

Output yang Dihasilkan:

text
3
[ 'Fiksi', 'Nonfiksi', 'Referensi' ]
6

Lalu ada mapReduce. Ia adalah mekanisme agregasi generasi pertama: kita menulis fungsi map dan reduce dalam JavaScript, dan MongoDB menjalankannya. Pada MongoDB 9.0 mapReduce masih bisa dijalankan, tetapi berstatus deprecated sejak MongoDB 5.0 — setiap pemanggilan memunculkan peringatan:

js
db.buku.mapReduce(
  function () { emit(this.kategori, 1); },
  function (key, nilai) { return Array.sum(nilai); },
  { out: { inline: 1 } }
)

Output yang Dihasilkan:

text
DeprecationWarning: Collection.mapReduce() is deprecated. Use an aggregation instead.
See https://mongodb.com/docs/manual/core/map-reduce for details.
{
  results: [
    { _id: 'Referensi', value: 1 },
    { _id: 'Fiksi', value: 3 },
    { _id: 'Nonfiksi', value: 2 }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1792256076, i: 7 }),
    signature: {
      hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
      keyId: Long('0')
    }
  },
  operationTime: Timestamp({ t: 1792256076, i: 7 })
}

Hasilnya sama dengan yang bisa dihasilkan $group: tiga kategori dengan jumlah bukunya masing-masing. Perbedaannya ada pada biayanya — fungsi JavaScript di mapReduce dieksekusi tanpa banyak optimasi, sementara pipeline dijalankan sebagai rangkaian operator native yang lebih cepat dan lebih mudah dibaca. Kita mungkin masih menjumpai mapReduce di kode basis lama; untuk kode baru, dokumentasi resmi mengarahkan seluruhnya ke aggregation pipeline.

Mengapa Aggregation Pipeline Menjadi Pilihan Utama #

Empat alasan menjelaskan posisi pipeline sebagai jalur utama:

  • Deklaratif dan berlapis: kita menyusun langkah pengolahan sebagai daftar tahap — saring, kelompokkan, urutkan — bukan sebagai fungsi yang harus kita tulis sendiri.
  • Berjalan di server: data tidak berpindah tempat; hanya hasil akhir yang dikirim ke klien.
  • Menyentuh index: tahap penyaringan di awal pipeline dapat memakai index, sama seperti query find().
  • Kosakata ekspresi yang kaya: aritmetika, string, tanggal, array, kondisi, hingga join antar-collection — semua tersedia sebagai operator siap pakai.

Perjalanan kita di Section ini mengikuti urutan yang alami: konsep pipeline, lalu tahap-tahap inti $match, $project, $set, $group, $sort, $limit/$skip, $unwind, $count, dan $lookup — masing-masing satu Chapter.

Rangkuman #

  • find() menjawab “document mana yang cocok”; aggregation menjawab “berapa nilainya, per kelompok apa, dalam bentuk apa”.
  • Menghitung di sisi klien memindahkan seluruh data lewat jaringan; aggregate() menghitung di server dan mengirim hasil akhir saja.
  • Tiga jalur agregasi: single-purpose method, aggregation pipeline, dan mapReduce yang berstatus deprecated sejak 5.0 namun masih berjalan di 9.0.
  • Pipeline menang karena deklaratif, server-side, index-aware, dan kaya operator ekspresi.