“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:
$bucketdenganboundaries,default, danoutputbuatan tangan.$bucketAutodengan jumlah bucket tertentu dangranularityseri 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:
| Field | Wajib | Peran |
|---|---|---|
groupBy | ya | ekspresi yang nilainya dikelompokkan (mis. "$harga") |
boundaries | ya | array batas, minimal dua, urut menaik, satu jenis angka |
default | tidak | label bucket penampung nilai di luar boundaries |
output | tidak | bentuk hasil tiap bucket; bawaan field count |
Siapkan data uji yang sama dengan Chapter $facet:
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:
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:
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:
[
{ _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:
_id: 0— rentang[0, 80000)menampungHujan Bulan Juni(65.000) danRumah Kaca(72.000); dua buku, 195 total eksemplar terjual._id: 80000— rentang[80000, 100000)menampungBumi,Filosofi Teras(keduanya 89.000), danLaut Bercerita(98.000). Nilai_idselalu batas bawah rentang._id: 100000— rentang[100000, 120000)untukBumi Manusia(105.000) danAtomic Habits(115.000).default—Ensiklopedia Sains Populer(250.000) berada di luar batas atas terakhir 120.000, sehingga jatuh ke bucketdefault. Tanpadefault, 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:
db.buku.aggregate([
{ $bucket: { groupBy: "$harga", boundaries: [0, 80000, 100000, 120000] } }
])Output yang Dihasilkan:
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 }:
db.buku.aggregate([
{ $bucketAuto: {
groupBy: "$harga",
buckets: 3,
output: { jumlahBuku: { $sum: 1 } }
} }
])Output yang Dihasilkan:
[
{ _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:
- Bucket pertama
[65000, 98000)menampung empat buku termurah — batasnya menempel pada nilai data, bukan angka bulat pilihan kita. - Bucket kedua
[98000, 250000)menampung tiga buku menengah;Laut Bercerita(98.000) masuk ke sini sebagai batas bawah. - Bucket terakhir
{ min: 250000, max: 250000 }hanya berisiEnsiklopedia Sains Populer— hanya ada satu nilai setelah 115.000, sehingga bucket penutup memilikiminsama denganmax. 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:
db.buku.aggregate([
{ $bucketAuto: {
groupBy: "$harga",
buckets: 3,
granularity: "R5",
output: { jumlahBuku: { $sum: 1 } }
} }
])Output yang Dihasilkan:
[
{ _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 #
$bucketmengelompokkan nilai ke rentang[bawah, atas)dariboundariesbuatan tangan; nilai di luar jangkauan butuhdefaultatau eksekusi gagal.$bucketAutomemilih sendiri batasnya agar terbentuk kira-kirabucketskelompok; hasil_idberupa{ 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/$bucketAutodi dalam cabang$facetuntuk 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.