Sebuah dashboard toko buku biasanya menampilkan beberapa angka sekaligus: total pendapatan, jumlah judul per kategori, tiga buku terlaris, dan sebaran harga. Tanpa $facet, kita harus menjalankan empat pipeline aggregate() yang berbeda — collection dibaca empat kali, dan di server produksi itu berarti empat kali biaya I/O. Stage $facet memungkinkan satu eksekusi menghasilkan semua angka tersebut sekaligus lewat beberapa pipeline cabang.

Peta materi Chapter ini:

  • Bentuk dan mekanisme $facet — satu input, banyak cabang, satu document hasil.
  • Membangun dashboard toko buku dengan empat cabang sekaligus.
  • Batasan $facet: dilarang bersarang, output harus muat 16 MB.

Cara Kerja $facet #

Bentuk stage-nya adalah kumpulan cabang bernama, masing-masing berisi pipeline lengkap:

js
// mdtest: skip — ilustrasi bentuk stage, bukan perintah lengkap
{ $facet: {
    namaCabang1: [ { $stage }, { $stage } ],
    namaCabang2: [ { $stage } ]
} }

Mekanismenya: document yang keluar dari tahap sebelum $facet dialirkan ke semua cabang secara paralel, lalu setiap cabang memproses salinannya sendiri. Hasil akhirnya tepat satu document — setiap field adalah nama cabang, dan nilainya selalu berupa array hasil pipeline cabang tersebut, meskipun cabang hanya menghasilkan satu document. Karena cabang-cabang berbagi input yang sama, $match yang berlaku untuk semua cabang sebaiknya diletakkan sebelum $facet, bukan diulang di dalam tiap cabang.

Dashboard Toko Buku dalam Satu Eksekusi #

Siapkan data uji — katalog delapan buku yang akan dipakai konsisten sampai akhir Section ini:

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 } ] }
])

Output yang Dihasilkan:

text
switched to db latihan_agregasi2
{ ok: 1, dropped: 'latihan_agregasi2' }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3a4624a2b9afc5019a327'),
    '1': ObjectId('6ad3a4624a2b9afc5019a328'),
    '2': ObjectId('6ad3a4624a2b9afc5019a329'),
    '3': ObjectId('6ad3a4624a2b9afc5019a32a'),
    '4': ObjectId('6ad3a4624a2b9afc5019a32b'),
    '5': ObjectId('6ad3a4624a2b9afc5019a32c'),
    '6': ObjectId('6ad3a4624a2b9afc5019a32d'),
    '7': ObjectId('6ad3a4624a2b9afc5019a32e')
  }
}

Sekarang dashboard-nya — empat cabang, empat pertanyaan berbeda, satu eksekusi:

js
db.buku.aggregate([
  { $facet: {
      totalPendapatan: [
        { $group: { _id: null, pendapatan: { $sum: { $multiply: ["$harga", "$terjual"] } } } }
      ],
      perKategori: [
        { $group: { _id: "$kategori", jumlahBuku: { $sum: 1 } } },
        { $sort: { jumlahBuku: -1, _id: 1 } }
      ],
      terlaris: [
        { $sort: { terjual: -1 } },
        { $limit: 3 },
        { $project: { _id: 0, judul: 1, terjual: 1 } }
      ],
      sebaranHarga: [
        { $bucket: { groupBy: "$harga", boundaries: [0, 80000, 100000, 200000], default: "di atas 200000" } }
      ]
  } }
])

Output yang Dihasilkan:

text
[
  {
    totalPendapatan: [ { _id: null, pendapatan: 124945000 } ],
    perKategori: [
      { _id: 'fiksi', jumlahBuku: 3 },
      { _id: 'nonfiksi', jumlahBuku: 2 },
      { _id: 'puisi', jumlahBuku: 1 },
      { _id: 'referensi', jumlahBuku: 1 },
      { _id: 'sastra', jumlahBuku: 1 }
    ],
    terlaris: [
      { judul: 'Atomic Habits', terjual: 310 },
      { judul: 'Bumi', terjual: 250 },
      { judul: 'Filosofi Teras', terjual: 180 }
    ],
    sebaranHarga: [
      { _id: 0, count: 2 },
      { _id: 80000, count: 3 },
      { _id: 100000, count: 2 },
      { _id: 'di atas 200000', count: 1 }
    ]
  }
]

Mari kita bedah keempat cabangnya:

  1. totalPendapatan — $group dengan _id: null melipat seluruh input menjadi satu document; harga × terjual dijumlahkan lewat $sum dan $multiply. Hasilnya tetap array berisi satu element.
  2. perKategori — mengelompokkan per kategori, menghitung jumlahBuku, lalu mengurutkan dari yang terbesar. $sort di dalam cabang tidak mengganggu cabang lain.
  3. terlaris — mengurutkan seluruh buku berdasarkan terjual menurun, memotong tiga teratas, lalu merapikan field yang ditampilkan.
  4. sebaranHarga — memakai $bucket untuk histogram harga; stage ini akan kita bahas tuntas di Chapter berikutnya.

Perhatikan strukturnya: empat pertanyaan yang biasanya menuntut empat round-trip ke server kini pulang dalam satu document. Aplikasi cukup membaca field cabang yang dibutuhkan — misalnya hasil.perKategori untuk mengisi grafik, dan hasil.terlaris untuk daftar unggulan.

Batasan $facet #

Kekuatan $facet datang dengan tiga batasan penting. Pertama, $facet tidak boleh bersarang di dalam $facet lain — server menolaknya dengan error eksplisit:

js
db.buku.aggregate([
  { $facet: { a: [ { $facet: { b: [ { $match: {} } ] } } ] } }
])

Output yang Dihasilkan:

text
Uncaught 
MongoServerError[Location40600]: $facet is not allowed to be used within a $facet stage

Kedua, stage penulisan seperti $out dan $merge juga ditolak di dalam cabang — cabang $facet hanya untuk menghitung, bukan menulis hasil. Ketiga, karena seluruh hasil dikemas menjadi satu document, total output semua cabang harus muat dalam batas document MongoDB sebesar 16 MB. Pada collection besar, cabang yang mengembalikan ribuan document (misalnya daftar lengkap tanpa $limit) berisiko menabrak batas tersebut.

Rangkuman #

  • $facet menjalankan banyak pipeline cabang atas input yang sama dalam satu eksekusi; hasilnya satu document dengan satu field (berupa array) per cabang.
  • Letakkan $match bersama sebelum $facet, dan $sort/$limit/$project spesifik di dalam cabang masing-masing.
  • $facet tidak boleh bersarang dan tidak boleh berisi $out/$merge; total output semua cabang harus muat 16 MB.

Cabang sebaranHarga di atas baru memakai $bucket sekilas. Chapter berikutnya membedah stage tersebut secara penuh — batasan rentang manual, bucket otomatis, hingga granularity.