Kita sudah melihat mengapa collection reguler mahal untuk data sensor. Sekarang kita pakai fitur yang dirancang khusus untuk pekerjaan itu: time series collection, tersedia sejak MongoDB 5.0. Kita hanya mendeklarasikan field waktu, field metadata, dan frekuensi data — server yang mengelola bucket, kompresi kolom, dan index di balik layar. Chapter ini membuat collection cuaca sungguhan, mengintip bucket internalnya, lalu menguji batas operasi update dan delete-nya secara langsung.

Membuat Collection Cuaca #

Pembuatannya lewat db.createCollection() dengan opsi timeseries. Tiga field wajib dipahami: timeField (nama field bertipe Date), metaField (identitas seri data), dan granularity (frekuensi datanya):

js
use latihan_timeseries
db.dropDatabase()
db.createCollection("cuaca", {
  timeseries: {
    timeField: "waktu",
    metaField: "sensor",
    granularity: "minutes"
  },
  expireAfterSeconds: 604800
})
db.cuaca.insertMany([
  { waktu: ISODate("2026-12-10T02:00:00Z"), sensor: { sensor_id: "JKT-01", kota: "Jakarta" }, suhu: 27.4, kelembapan: 78 },
  { waktu: ISODate("2026-12-10T02:01:00Z"), sensor: { sensor_id: "JKT-01", kota: "Jakarta" }, suhu: 27.6, kelembapan: 77 },
  { waktu: ISODate("2026-12-10T02:02:00Z"), sensor: { sensor_id: "JKT-01", kota: "Jakarta" }, suhu: 27.9, kelembapan: 75 },
  { waktu: ISODate("2026-12-10T02:03:00Z"), sensor: { sensor_id: "BDG-07", kota: "Bandung" }, suhu: 22.1, kelembapan: 85 }
])
db.cuaca.find({}, { _id: 0, waktu: 1, sensor: 1, suhu: 1 }).sort({ waktu: 1 })

Output yang Dihasilkan:

text
switched to db latihan_timeseries
{ ok: 1, dropped: 'latihan_timeseries' }
{ ok: 1 }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3dff997eae5fc8738dccb'),
    '1': ObjectId('6ad3dff997eae5fc8738dccc'),
    '2': ObjectId('6ad3dff997eae5fc8738dccd'),
    '3': ObjectId('6ad3dff997eae5fc8738dcce')
  }
}
[
  {
    waktu: ISODate('2026-12-10T02:00:00.000Z'),
    sensor: { kota: 'Jakarta', sensor_id: 'JKT-01' },
    suhu: 27.4
  },
  {
    waktu: ISODate('2026-12-10T02:01:00.000Z'),
    sensor: { kota: 'Jakarta', sensor_id: 'JKT-01' },
    suhu: 27.6
  },
  {
    waktu: ISODate('2026-12-10T02:02:00.000Z'),
    sensor: { kota: 'Jakarta', sensor_id: 'JKT-01' },
    suhu: 27.9
  },
  {
    waktu: ISODate('2026-12-10T02:03:00.000Z'),
    sensor: { kota: 'Bandung', sensor_id: 'BDG-07' },
    suhu: 22.1
  }
]

Mari kita bedah konfigurasinya:

  1. timeField: "waktu" — setiap measurement wajib membawa field ini bertipe Date; nilainya menentukan urutan dan posisi data di dalam bucket.
  2. metaField: "sensor" — nilai field ini menjadi identitas seri: pembacaan dengan sensor yang sama dikumpulkan ke bucket yang sama.
  3. granularity: "minutes" — memberi tahu server bahwa data datang kira-kira per menit, sehingga rentang bucket diatur per 24 jam.
  4. expireAfterSeconds: 604800 — TTL: measurement otomatis dihapus saat waktu-nya berumur lebih dari 7 hari; ambangnya dihitung dari nilai timeField, bukan waktu insert.

Dari sisi aplikasi, insertMany dan find terasa persis seperti collection biasa — perbedaannya sepenuhnya di penyimpanan internal.

Mengintip Bucket Internal #

db.getCollectionInfos() mengonfirmasi tipe collection-nya, dan opsi rawData pada perintah find membuka bentuk mentah bucket-nya:

js
db.getCollectionInfos({ name: "cuaca" })
db.runCommand({ find: "cuaca", rawData: true, filter: {} })

Output yang Dihasilkan:

text
[
  {
    name: 'cuaca',
    type: 'timeseries',
    options: {
      expireAfterSeconds: Long('604800'),
      timeseries: {
        timeField: 'waktu',
        metaField: 'sensor',
        granularity: 'minutes',
        bucketMaxSpanSeconds: 86400,
        fixedBucketing: true
      }
    },
    info: { readOnly: false, uuid: UUID('cfe87688-404d-4054-8c19-a6ef0cdfdcab') }
  }
]
{
  cursor: {
    firstBatch: [
      {
        _id: ObjectId('6b1a07a0387cb63c7f8ee9b3'),
        control: {
          version: 2,
          min: {
            waktu: ISODate('2026-12-10T02:00:00.000Z'),
            suhu: 22.1,
            kelembapan: 85,
            _id: ObjectId('6ad3dff997eae5fc8738dcce')
          },
          max: {
            waktu: ISODate('2026-12-10T02:03:00.000Z'),
            suhu: 22.1,
            kelembapan: 85,
            _id: ObjectId('6ad3dff997eae5fc8738dcce')
          },
          count: 1
        },
        meta: { kota: 'Bandung', sensor_id: 'BDG-07' },
        data: {
          _id: Binary.createFromBase64('BwBq09/5l+rl/Ic43M4A', 7),
          kelembapan: Binary.createFromBase64('EABVAAAAAA==', 7),
          waktu: Binary.createFromBase64('CQAgiLBdogEAAAA=', 7),
          suhu: Binary.createFromBase64('AQCamZmZmRk2QAA=', 7)
        }
      },
      {
        _id: ObjectId('6b1a07a0387cb63c7f8ee9b4'),
        control: {
          version: 2,
          min: {
            waktu: ISODate('2026-12-10T02:00:00.000Z'),
            suhu: 27.4,
            kelembapan: 75,
            _id: ObjectId('6ad3dff997eae5fc8738dccb')
          },
          max: {
            waktu: ISODate('2026-12-10T02:02:00.000Z'),
            suhu: 27.9,
            kelembapan: 78,
            _id: ObjectId('6ad3dff997eae5fc8738dccd')
          },
          count: 3
        },
        meta: { kota: 'Jakarta', sensor_id: 'JKT-01' },
        data: {
          _id: Binary.createFromBase64('BwBq09/5l+rl/Ic43MuALQAAAAAAAAAA', 7),
          kelembapan: Binary.createFromBase64('EABOAAAAgB0AAAAMAAAAAA==', 7),
          waktu: Binary.createFromBase64('CQAAya1dogEAAIANTB0AAAAAAAA=', 7),
          suhu: Binary.createFromBase64('AQBmZmZmZmY7QKBNAAAAGAAAAAA=', 7)
        }
      }
    ],
    id: Long('0'),
    ns: 'latihan_timeseries.cuaca'
  },
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1792270329, i: 4 }),
    signature: {
      hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
      keyId: Long('0')
    }
  },
  operationTime: Timestamp({ t: 1792270329, i: 4 })
}

Empat pembacaan ternyata disimpan sebagai dua bucket. Anatomi setiap bucket:

  1. meta — nilai metaField disimpan sekali untuk seluruh isinya; inilah sumber penghematan terbesar.
  2. data — setiap field measurement (waktu, suhu, kelembapan) menjadi satu kolom biner terkompresi; nilai-nilai sejenis berdampingan sehingga kompresi bekerja optimal.
  3. control — ringkasan min/max/count per kolom, dipakai server untuk memangkas bucket saat query rentang waktu tanpa membongkar isinya.

Perhatikan juga keluaran getCollectionInfos: granularity: "minutes" diterjemahkan menjadi bucketMaxSpanSeconds: 86400 — satu bucket menampung maksimum data 24 jam. Sejak MongoDB 9.0 seluruh data ini hidup dalam satu namespace; mengakses system.buckets.cuaca secara langsung ditolak server, dan rawData adalah jalur resmi untuk membaca format mentah.

Batasan Update dan Delete #

Time series collection dirancang untuk data yang tidak berubah, tetapi bukan berarti sepenuhnya read-only. Sejak MongoDB 5.1, update dan delete diizinkan dengan syarat ketat: filter hanya boleh menyentuh metaField, dan update hanya boleh memodifikasi metaField. Kita uji ketiganya:

js
db.cuaca.updateOne({ waktu: ISODate("2026-12-10T02:03:00Z") }, { $set: { suhu: 22.2 } })

Output yang Dihasilkan:

text
Uncaught 
MongoServerError: Command error committing internal transaction :: caused by :: Cannot perform a non-multi update on a time-series collection

Galat pertama muncul karena updateOne selalu mengirim multi: false. Ganti ke updateMany dengan filter metaField (pakai dot notation agar bebas dari urutan field subdocument):

js
db.cuaca.updateMany({ "sensor.sensor_id": "BDG-07" }, { $set: { sensor: { sensor_id: "BDG-07", kota: "Kota Bandung" } } })
db.cuaca.find({ "sensor.sensor_id": "BDG-07" }, { _id: 0, waktu: 1, suhu: 1 })

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
[ { waktu: ISODate('2026-12-10T02:03:00.000Z'), suhu: 22.1 } ]

Operasi berikutnya berhasil karena filter dan perubahannya sama-sama menyentuh metaField saja — mengoreksi nama kota pada seluruh pembacaan sensor tersebut. Sebaliknya, mengubah suhu akan ditolak dengan pesan galat serupa (“updates a field that is not the metaField”). Untuk delete, aturan yang sama berlaku: filter hanya boleh metaField atau _id:

js
db.cuaca.deleteOne({ _id: db.cuaca.findOne({ "sensor.sensor_id": "BDG-07" })._id })
db.cuaca.countDocuments()

Output yang Dihasilkan:

text
{ acknowledged: true, deletedCount: 1 }
3

Batasan lain yang perlu dicatat: document maksimum 4 MB (bukan 16 MB), tidak bisa diubah menjadi tipe collection lain, tidak mendukung schema validation, dan tidak bisa ditulis di dalam transaksi. Change streams juga tidak tersedia untuk time series collection — kita akan melihat buktinya di Chapter Querying Time Series.

Rangkuman #

  • db.createCollection() dengan opsi timeseries (timeField, metaField, granularity) plus expireAfterSeconds membuat collection yang tampak normal dari luar tetapi berformat bucket kolom di dalam.
  • rawData: true pada find memperlihatkan bucket mentah: meta sekali per seri, data berupa kolom terkompresi, control menyimpan min/max/count untuk pemangkasan query.
  • Update/delete hanya boleh menyentuh metaField (filter dan perubahan), wajib multi: true; koreksi data pengukuran berarti tulis ulang measurement baru.
  • Retensi data sebaiknya diserahkan ke expireAfterSeconds, bukan delete massal.