Data sensor baru berguna kalau bisa ditanya jawab: seberapa panas Jakarta antara jam 02:00 dan 03:00? Sensor mana yang paling fluktuatif hari ini? Bagaimana ringkasan per jam untuk dasbor? Kabar baiknya: semua query biasa — find, sort, aggregation pipeline — bekerja di time series collection tanpa sintaks khusus. Bab ini menyusun pola query khas time series: filter rentang waktu, agregasi per seri, pemotongan per jam, dan downsampling, lalu menutup dengan satu batasan penting yang kita buktikan langsung.

Filter Rentang Waktu dan metaField #

Dua dimensi filter time series adalah waktu dan identitas seri. Gabungkan keduanya dengan operator perbandingan tanggal:

js
use latihan_timeseries
db.dropDatabase()
db.createCollection("cuaca", { timeseries: { timeField: "waktu", metaField: "sensor", granularity: "minutes" } })
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:10:00Z"), sensor: { sensor_id: "JKT-01", kota: "Jakarta" }, suhu: 27.6, kelembapan: 77 },
  { waktu: ISODate("2026-12-10T02:20:00Z"), sensor: { sensor_id: "JKT-01", kota: "Jakarta" }, suhu: 27.9, kelembapan: 75 },
  { waktu: ISODate("2026-12-10T02:30:00Z"), sensor: { sensor_id: "JKT-01", kota: "Jakarta" }, suhu: 28.1, kelembapan: 74 },
  { waktu: ISODate("2026-12-10T02:40:00Z"), sensor: { sensor_id: "BDG-07", kota: "Bandung" }, suhu: 22.1, kelembapan: 85 },
  { waktu: ISODate("2026-12-10T03:00:00Z"), sensor: { sensor_id: "BDG-07", kota: "Bandung" }, suhu: 22.3, kelembapan: 84 },
  { waktu: ISODate("2026-12-10T03:20:00Z"), sensor: { sensor_id: "SBY-12", kota: "Surabaya" }, suhu: 30.2, kelembapan: 68 },
  { waktu: ISODate("2026-12-10T03:40:00Z"), sensor: { sensor_id: "SBY-12", kota: "Surabaya" }, suhu: 30.5, kelembapan: 66 }
])
db.cuaca.find({ "sensor.kota": "Jakarta", waktu: { $gte: ISODate("2026-12-10T02:15:00Z"), $lt: ISODate("2026-12-10T03:00:00Z") } }, { _id: 0, waktu: 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('6ad3e0eead650c033ffec21a'),
    '1': ObjectId('6ad3e0eead650c033ffec21b'),
    '2': ObjectId('6ad3e0eead650c033ffec21c'),
    '3': ObjectId('6ad3e0eead650c033ffec21d'),
    '4': ObjectId('6ad3e0eead650c033ffec21e'),
    '5': ObjectId('6ad3e0eead650c033ffec21f'),
    '6': ObjectId('6ad3e0eead650c033ffec220'),
    '7': ObjectId('6ad3e0eead650c033ffec221')
  }
}
[
  { waktu: ISODate('2026-12-10T02:20:00.000Z'), suhu: 27.9 },
  { waktu: ISODate('2026-12-10T02:30:00.000Z'), suhu: 28.1 }
]

Filter ini adalah pola 90% query time series: satu kesamaan metaField (dot notation!) plus satu rentang timeField. Index otomatis { sensor: 1, waktu: 1 } melayani persis bentuk ini, dan pemangkasan control.min/control.max memastikan bucket di luar rentang tidak dibaca. Batas rentang sebaiknya selalu setengah terbuka seperti di atas — $gte untuk awal, $lt untuk akhir — agar titik tepat di batas tidak terhitung ganda saat rentang disambung.

Agregasi per Sensor dan per Jam #

Pertanyaan analitik dijawab aggregation pipeline. Yang pertama: statistik per sensor sepanjang hari:

js
db.cuaca.aggregate([ { $group: { _id: "$sensor.kota", rataSuhu: { $avg: "$suhu" }, suhuMaks: { $max: "$suhu" }, suhuMin: { $min: "$suhu" }, pembacaan: { $sum: 1 } } }, { $sort: { _id: 1 } } ])

Output yang Dihasilkan:

text
[
  { _id: 'Bandung', rataSuhu: 22.200000000000003, suhuMaks: 22.3, suhuMin: 22.1, pembacaan: 2 },
  { _id: 'Jakarta', rataSuhu: 27.75, suhuMaks: 28.1, suhuMin: 27.4, pembacaan: 4 },
  { _id: 'Surabaya', rataSuhu: 30.35, suhuMaks: 30.5, suhuMin: 30.2, pembacaan: 2 }
]

Yang kedua memotong waktu per jam dengan $dateTrunc — operator yang juga kita pakai untuk membangun Bucket Pattern manual, kini bekerja langsung di atas time series collection:

js
db.cuaca.aggregate([ { $match: { "sensor.kota": "Jakarta" } }, { $group: { _id: { $dateTrunc: { date: "$waktu", unit: "hour" } }, rataSuhu: { $avg: "$suhu" }, pembacaan: { $sum: 1 } } }, { $sort: { _id: 1 } } ])

Output yang Dihasilkan:

text
[
  {
    _id: ISODate('2026-12-10T02:00:00.000Z'),
    rataSuhu: 27.75,
    pembacaan: 4
  }
]

$dateTrunc memotong tiap waktu ke awal jam, sehingga pembacaan 02:00–02:59 berbagi kunci grup yang sama — keempat pembacaan Jakarta jatuh ke satu grup jam 02:00. unit bisa dinaikkan ke "day" atau "week" untuk ringkasan yang lebih kasar. Pola $match dulu baru $group tetap penting: sensor.kota disaring sebelum data dihitung, memanfaatkan index dan pemangkasan bucket.

Downsampling ke Collection Ringkasan #

Data mentah per 10 menit tidak perlu disajikan ke grafik setahun. Pipeline downsampling meringkasnya ke collection biasa lewat $out:

js
db.cuaca.aggregate([ { $group: { _id: { kota: "$sensor.kota", jam: { $dateTrunc: { date: "$waktu", unit: "hour" } } }, rataSuhu: { $avg: "$suhu" }, pembacaan: { $sum: 1 } } }, { $project: { _id: 0, kota: "$_id.kota", jam: "$_id.jam", rataSuhu: 1, pembacaan: 1 } }, { $out: "cuaca_ringkas" } ])
db.cuaca_ringkas.find().sort({ kota: 1, jam: 1 })

Output yang Dihasilkan:

text
[
  {
    _id: ObjectId('6ad3e0ef9d08d4e3fde1a47b'),
    rataSuhu: 22.1,
    pembacaan: 1,
    kota: 'Bandung',
    jam: ISODate('2026-12-10T02:00:00.000Z')
  },
  {
    _id: ObjectId('6ad3e0ef9d08d4e3fde1a479'),
    rataSuhu: 22.3,
    pembacaan: 1,
    kota: 'Bandung',
    jam: ISODate('2026-12-10T03:00:00.000Z')
  },
  {
    _id: ObjectId('6ad3e0ef9d08d4e3fde1a47a'),
    rataSuhu: 27.75,
    pembacaan: 4,
    kota: 'Jakarta',
    jam: ISODate('2026-12-10T02:00:00.000Z')
  },
  {
    _id: ObjectId('6ad3e0ef9d08d4e3fde1a478'),
    rataSuhu: 30.35,
    pembacaan: 2,
    kota: 'Surabaya',
    jam: ISODate('2026-12-10T03:00:00.000Z')
  }
]

Pipeline ini membaca dari time series collection (diizinkan) dan menulis ke collection reguler cuaca_ringkas (juga diizinkan) — $out mengganti isi collection target secara utuh setiap dijalankan, dan _id baru dibuatkan untuk tiap baris ringkasan. Arah sebaliknya tidak: $merge dan $out tidak bisa menulis ke dalam time series collection — jalur resmi mengisi time series adalah insert. Jalankan pipeline seperti ini secara berkala (cron, Atlas trigger, atau job harian) dan dasbor membaca ringkasan kecil alih-alih jutaan pengukuran.

Batasan yang Perlu Diketahui #

Sebagian besar operasi terasa normal, tetapi beberapa pintu memang sengaja ditutup. Yang paling mengejutkan bagi banyak developer: change streams tidak tersedia untuk time series collection. Kita buktikan:

js
db.cuaca.watch()

Output yang Dihasilkan:

text
Uncaught 
MongoServerError[CommandNotSupported]: Cannot run aggregation on timeseries with namespace latihan_timeseries.cuaca

Galat ini muncul karena change stream dibangun sebagai aggregation di atas oplog dengan bentuk khusus, dan bentuk itu tidak diterapkan pada namespace time series. Pantau perubahan data cuaca dari sisi aplikasi (misalnya tulis event ke collection biasa saat data masuk), bukan lewat stream. Batasan lain yang sudah kita bahas: update/delete hanya pada metaField, $merge tidak bisa menargetkan time series, index unique dan text tidak didukung, dan document maksimum 4 MB.

Rangkuman #

  • Pola query utama time series: equality metaField (dot notation) + rentang timeField setengah terbuka — dilayani index otomatis dan pemangkasan bucket.
  • $group dengan $avg/$max/$min menjawab statistik per seri; $dateTrunc memotong waktu per jam/hari untuk deret ringkasan.
  • Downsampling: aggregate dari time series lalu $out ke collection reguler; jalur masuk data tetap insert saja.
  • Change streams ditolak di time series collection — terbukti dari galat CommandNotSupported di atas; pantau perubahan dari sisi aplikasi.