Satu sensor suhu di gudang buku mengirim pembacaan setiap 10 detik — 8.640 document per hari, 3,1 juta per tahun, dan itu baru satu sensor. Menyimpan setiap pembacaan sebagai document sendiri mengulang field sensorId serta timestamp ribuan kali dan menuntut index raksasa. Bucket Pattern mengelompokkan pembacaan ke dalam wadah per rentang waktu — per menit, per jam — sehingga data yang bersebelahan hidup bersama. Chapter ini membangun bucket nyata, memverifikasinya lewat aggregation, dan mengukur penghematan storage-nya.

Masalah: Satu Document per Pembacaan #

Document terkecil sekalipun membawa overhead tetap: key, tipe, panjang, _id, plus entry index. Pembacaan 26.4 derajat hanya butuh dua angka, tetapi document-nya mengulang sensorId: "S-01" dan struktur yang sama setiap 10 detik. Pada jutaan pembacaan, overhead berulang ini bisa melampaui data suhu itu sendiri — dan setiap query analisis harus menyisir jutaan document.

Bucket per Menit #

Bentuk bucketnya: satu document per sensor per menit, berisi array nilai, jumlah sampel, serta min dan max yang sudah dihitung. Dulu kita simpan dulu data mentahnya:

js
use latihan_modeling
db.dropDatabase()
db.sensor_raw.insertMany([
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:05:10Z"), suhu: 26.4 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:05:20Z"), suhu: 26.6 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:05:30Z"), suhu: 26.5 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:05:40Z"), suhu: 26.7 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:05:50Z"), suhu: 26.8 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:05:59Z"), suhu: 26.6 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:06:10Z"), suhu: 27.0 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:06:20Z"), suhu: 27.2 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:06:30Z"), suhu: 27.1 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:06:40Z"), suhu: 27.3 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:06:50Z"), suhu: 27.4 },
  { sensorId: "S-01", waktu: ISODate("2026-12-06T09:06:59Z"), suhu: 27.2 }
])

Output yang Dihasilkan:

text
switched to db latihan_modeling
{ ok: 1, dropped: 'latihan_modeling' }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39ade6594080746c2dd0a'),
    '1': ObjectId('6ad39ade6594080746c2dd0b'),
    '2': ObjectId('6ad39ade6594080746c2dd0c'),
    '3': ObjectId('6ad39ade6594080746c2dd0d'),
    '4': ObjectId('6ad39ade6594080746c2dd0e'),
    '5': ObjectId('6ad39ade6594080746c2dd0f'),
    '6': ObjectId('6ad39ade6594080746c2dd10'),
    '7': ObjectId('6ad39ade6594080746c2dd11'),
    '8': ObjectId('6ad39ade6594080746c2dd12'),
    '9': ObjectId('6ad39ade6594080746c2dd13'),
    '10': ObjectId('6ad39ade6594080746c2dd14'),
    '11': ObjectId('6ad39ade6594080746c2dd15')
  }
}

Dua belas document mentah untuk dua menit pengamatan. Versi bucket-nya menampung menit yang sama dalam dua document:

js
db.sensor_bucket.insertMany([
  { sensorId: "S-01", menit: ISODate("2026-12-06T09:05:00Z"), nilai: [26.4, 26.6, 26.5, 26.7, 26.8, 26.6], jumlah: 6, min: 26.4, max: 26.8 },
  { sensorId: "S-01", menit: ISODate("2026-12-06T09:06:00Z"), nilai: [27.0, 27.2, 27.1, 27.3, 27.4, 27.2], jumlah: 6, min: 27.0, max: 27.4 }
])
db.sensor_bucket.find({ sensorId: "S-01" }, { _id: 0, menit: 1, jumlah: 1, min: 1, max: 1 })

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39ade6594080746c2dd16'),
    '1': ObjectId('6ad39ade6594080746c2dd17')
  }
}
[
  {
    menit: ISODate('2026-12-06T09:05:00.000Z'),
    jumlah: 6,
    min: 26.4,
    max: 26.8
  },
  {
    menit: ISODate('2026-12-06T09:06:00.000Z'),
    jumlah: 6,
    min: 27,
    max: 27.4
  }
]

Satu bucket menyimpan enam pembacaan: field menit menjadi identitas rentang, array nilai menampung urutan pembacaan, dan jumlah/min/max adalah ringkasan siap baca. Query “suhu tertinggi per menit” kini membaca satu field, bukan memindai enam document dan menghitung ulang.

Ukuran rentang bucket adalah keputusan desain tersendiri: menit menangkap granularitas tinggi tetapi menghasilkan lebih banyak document; jam menekan jumlah document tetapi menyimpan 360 pembacaan dalam satu array. Patokannya kebutuhan query — kalau dasbor membaca per menit, bucket per menit; kalau analisis mingguan cukup per jam, bucket per jam. Batas atasnya tetap 16 MB per document, jadi rentang yang terlalu panjang dengan frekuensi terlalu rapat bisa menabraknya.

Membangun Bucket dari Data Mentah #

Bucket tidak harus ditulis tangan — aggregation $dateTrunc mampu memotong timestamp ke awal menit lalu mengelompokkannya. Inilah verifikasi bahwa bucket kita jujur terhadap data mentah:

js
db.sensor_raw.aggregate([
  { $group: {
      _id: { sensorId: "$sensorId", menit: { $dateTrunc: { date: "$waktu", unit: "minute" } } },
      jumlah: { $sum: 1 },
      min: { $min: "$suhu" },
      max: { $max: "$suhu" }
  } },
  { $sort: { "_id.menit": 1 } }
])

Output yang Dihasilkan:

text
[
  {
    _id: { sensorId: 'S-01', menit: ISODate('2026-12-06T09:05:00.000Z') },
    jumlah: 6,
    min: 26.4,
    max: 26.8
  },
  {
    _id: { sensorId: 'S-01', menit: ISODate('2026-12-06T09:06:00.000Z') },
    jumlah: 6,
    min: 27,
    max: 27.4
  }
]

Bedah pipeline-nya:

  1. $dateTrunc dengan unit: "minute" — memotong 09:05:59 menjadi 09:05:00; semua pembacaan dalam menit yang sama kini bernilai kunci grup sama.
  2. $group dengan _id gabungan — sensorId + menit menjadi identitas bucket; persis pasangan yang kita pilih sebagai identitas document bucket.
  3. $min dan $max — ringkasan dihitung server; hasilnya identik dengan yang kita tulis manual di sensor_bucket.

Pipeline seperti ini biasa dipakai sebagai tugas latar untuk membangun bucket dari data mentah yang terus mengalir.

Hemat Storage Secara Terukur #

Operator $bsonSize mengembalikan ukuran BSON document, dan $sum menjumlahkannya — alat yang sama dengan Chapter Document Growth, dipakai untuk membandingkan kedua representasi:

js
db.sensor_raw.aggregate([ { $group: { _id: null, jumlahDocument: { $sum: 1 }, totalByte: { $sum: { $bsonSize: "$$ROOT" } } } } ])
db.sensor_bucket.aggregate([ { $group: { _id: null, jumlahDocument: { $sum: 1 }, totalByte: { $sum: { $bsonSize: "$$ROOT" } } } } ])

Output yang Dihasilkan:

text
[ { _id: null, jumlahDocument: 12, totalByte: 836 } ]
[ { _id: null, jumlahDocument: 2, totalByte: 336 } ]

Data yang sama memakan 836 byte dalam 12 document mentah, atau 336 byte dalam 2 document bucket — hemat hampir 60% bahkan pada contoh sekecil ini. Komposisinya yang penting: overhead per document (_id, nama field, entry index) dibayar sekali per bucket, bukan sekali per pembacaan. Semakin panjang rentang bucket dan semakin rapat frekuensi pengukuran, semakin besar porsinya overhead yang tertekan — pada sensor per detik dengan bucket per jam, ribuan pembacaan berbagi satu sensorId dan satu menit.

Rangkuman #

  • Bucket Pattern mengelompokkan data deret waktu per rentang (menit/jam) ke dalam satu document berisi array nilai plus ringkasan jumlah, min, max.
  • $dateTrunc + $group adalah resep membangun dan memverifikasi bucket dari data mentah.
  • $bsonSize + $sum mengukur penghematan storage secara konkret; overhead per document dibayar sekali per bucket.
  • Time series collections (MongoDB 5.0+) mengotomatiskan pola ini bila kendali manual tidak dibutuhkan.

Satu masalah data besar belum terjawab: bagaimana bila satu document jutaan kali lebih panas dari document lain? Itu wilayah Outlier Pattern, chapter berikutnya.