“Buku dengan harga di bawah Rp80.000 ada berapa? Antara Rp80.000 dan Rp100.000?” — pertanyaan berbentuk histogram seperti ini muncul setiap kali kita ingin memahami sebaran data, bukan sekadar nilai rata-rata. Membangunnya dengan $group saja menuntut $switch atau rentang bersyarat yang panjang. Stage $bucket mengerjakannya dengan satu deklarasi rentang, sedangkan $bucketAuto bahkan memilih rentangnya sendiri. Chapter ini menguji keduanya pada sebaran harga katalog toko buku.

Peta materi Chapter ini:

  • $bucket dengan boundaries, default, dan output buatan tangan.
  • $bucketAuto dengan jumlah bucket tertentu dan granularity seri R5/POWERSOF2.

$bucket: Rentang Buatan Tangan #

$bucket memotong nilai dari groupBy ke dalam rentang setengah-terbuka [batas-bawah, batas-atas) — batas bawah ikut, batas atas tidak. Fieldnya:

FieldWajibPeran
groupByyaekspresi yang nilainya dikelompokkan (mis. "$harga")
boundariesyaarray batas, minimal dua, urut menaik, satu jenis angka
defaulttidaklabel bucket penampung nilai di luar boundaries
outputtidakbentuk hasil tiap bucket; bawaan field count

Siapkan data uji yang sama dengan Chapter $facet:

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('6ad3a4a72a03a8f94ae348b3'),
    '1': ObjectId('6ad3a4a72a03a8f94ae348b4'),
    '2': ObjectId('6ad3a4a72a03a8f94ae348b5'),
    '3': ObjectId('6ad3a4a72a03a8f94ae348b6'),
    '4': ObjectId('6ad3a4a72a03a8f94ae348b7'),
    '5': ObjectId('6ad3a4a72a03a8f94ae348b8'),
    '6': ObjectId('6ad3a4a72a03a8f94ae348b9'),
    '7': ObjectId('6ad3a4a72a03a8f94ae348ba')
  }
}

Histogram harga tiga rentang, plus penampung untuk harga di luar batas:

js
db.buku.aggregate([
  { $bucket: {
      groupBy: "$harga",
      boundaries: [0, 80000, 100000, 120000],
      default: "di atas 120000",
      output: { jumlahBuku: { $sum: 1 }, totalTerjual: { $sum: "$terjual" } }
  } }
])

Output yang Dihasilkan:

text
[
  { _id: 0, jumlahBuku: 2, totalTerjual: 195 },
  { _id: 80000, jumlahBuku: 3, totalTerjual: 550 },
  { _id: 100000, jumlahBuku: 2, totalTerjual: 460 },
  { _id: 'di atas 120000', jumlahBuku: 1, totalTerjual: 40 }
]

Bedah hasilnya per baris:

  1. _id: 0 — rentang [0, 80000) menampung Hujan Bulan Juni (65.000) dan Rumah Kaca (72.000); dua buku, 195 total eksemplar terjual.
  2. _id: 80000 — rentang [80000, 100000) menampung Bumi, Filosofi Teras (keduanya 89.000), dan Laut Bercerita (98.000). Nilai _id selalu batas bawah rentang.
  3. _id: 100000 — rentang [100000, 120000) untuk Bumi Manusia (105.000) dan Atomic Habits (115.000).
  4. default — Ensiklopedia Sains Populer (250.000) berada di luar batas atas terakhir 120.000, sehingga jatuh ke bucket default. Tanpa default, pipeline ini gagal.

Perilaku terakhir itu penting: boundaries tidak otomatis menampung nilai yang lebih besar dari batas atas. Kita bisa melihat error-nya secara langsung:

js
db.buku.aggregate([
  { $bucket: { groupBy: "$harga", boundaries: [0, 80000, 100000, 120000] } }
])

Output yang Dihasilkan:

text
Uncaught 
MongoServerError[Location7158303]: Executor error during aggregate command on namespace: latihan_agregasi2.buku :: caused by :: $switch could not find a matching branch for an input, and no default was specified.

Pesan error tersebut menyebut $switch karena secara internal $bucket memang dibangun di atas $switch — setiap rentang menjadi satu case. Operator $switch sendiri akan kita bahas di Chapter Conditional Expressions.

$bucketAuto: Jumlah Bucket Ditetapkan #

Bila kita tidak tahu rentang mana yang bermakna, $bucketAuto membagi data menjadi kira-kira buckets bucket dengan batas yang dihitung dari sebaran nilai — kira-kira, karena jumlah bucket akhirnya bisa lebih sedikit bila nilai distinct terbatas. Field _id hasilnya berupa { min, max }:

js
db.buku.aggregate([
  { $bucketAuto: {
      groupBy: "$harga",
      buckets: 3,
      output: { jumlahBuku: { $sum: 1 } }
  } }
])

Output yang Dihasilkan:

text
[
  { _id: { min: 65000, max: 98000 }, jumlahBuku: 4 },
  { _id: { min: 98000, max: 250000 }, jumlahBuku: 3 },
  { _id: { min: 250000, max: 250000 }, jumlahBuku: 1 }
]

Mari kita baca hasilnya:

  1. Bucket pertama [65000, 98000) menampung empat buku termurah — batasnya menempel pada nilai data, bukan angka bulat pilihan kita.
  2. Bucket kedua [98000, 250000) menampung tiga buku menengah; Laut Bercerita (98.000) masuk ke sini sebagai batas bawah.
  3. Bucket terakhir { min: 250000, max: 250000 } hanya berisi Ensiklopedia Sains Populer — hanya ada satu nilai setelah 115.000, sehingga bucket penutup memiliki min sama dengan max. Perilaku ini normal saat nilai distinct lebih sedikit daripada bucket yang diminta.

Granularity: Batas yang Ramah untuk Dibaca #

Batas hasil $bucketAuto seperti 98000 sulit dibaca di laporan. Opsi granularity membulatkan batas ke seri angka yang dikenal — "R5" memakai seri 1-2-5 (…, 10, 20, 50, 100, …) dikalikan pangkat sepuluh, sedangkan "POWERSOF2" memakai pangkat dua. Pilihan seri mengikuti satuan data: uang rupiah cocok dengan R5, ukuran byte memori cocok dengan POWERSOF2:

js
db.buku.aggregate([
  { $bucketAuto: {
      groupBy: "$harga",
      buckets: 3,
      granularity: "R5",
      output: { jumlahBuku: { $sum: 1 } }
  } }
])

Output yang Dihasilkan:

text
[
  { _id: { min: 63000, max: 100000 }, jumlahBuku: 5 },
  { _id: { min: 100000, max: 400000 }, jumlahBuku: 3 }
]

Setelah dibulatkan, batas-batasnya menjadi 63.000, 100.000, dan 400.000 — angka-angka seri 1-2-5 yang jauh lebih enak dibaca di sumbu grafik. Perhatikan pula bahwa jumlah bucket berkurang menjadi dua: pembulatan dapat membuat dua batas bertemu sehingga bucket bersebelahan melebur. Itu perilaku normal, bukan error.

Rangkuman #

  • $bucket mengelompokkan nilai ke rentang [bawah, atas) dari boundaries buatan tangan; nilai di luar jangkauan butuh default atau eksekusi gagal.
  • $bucketAuto memilih sendiri batasnya agar terbentuk kira-kira buckets kelompok; hasil _id berupa { min, max } dan jumlah bucket bisa lebih sedikit dari permintaan.
  • granularity: "R5" atau "POWERSOF2" membulatkan batas ke seri angka yang mudah dibaca; bucket bisa melebur setelah pembulatan.
  • Kombinasi klasik: $bucket/$bucketAuto di dalam cabang $facet untuk panel histogram sebuah dashboard.

Hasil agregasi kadang perlu dirombak total bentuknya — misalnya menjadikan document bersarang sebagai document utama. $replaceRoot dan $replaceWith mengerjakannya, bahan Chapter berikutnya.