Penyiapan lab

TimescaleDB dan InfluxDB 3.x di dalam container, disertai pemuatan pertama telemetri contoh.

Daftar isi

Lab pada jalur ini menggunakan dua mesin, yaitu TimescaleDB sebagai ekstensi Postgres dan InfluxDB 3 Core, keduanya dijalankan di dalam container. Topik ini menyiapkan kedua mesin secara konseptual, membangkitkan telemetri contoh yang digunakan berulang pada jalur ini, dan menunjukkan dua jalur data berbeda, yaitu skema relasional hypertable dan line protocol InfluxDB. Setelah membaca topik ini, dataset contoh berupa telemetri sensor siap pakai, dan bentuk skema pada kedua mesin menjadi jelas sebelum benar-benar dijalankan.

Dua mesin untuk satu dataset#

TimescaleDB adalah ekstensi Postgres, sehingga klien yang dapat berbicara dengan Postgres dapat berbicara dengan TimescaleDB tanpa perubahan. Hypertable adalah abstraksi tabel yang secara otomatis dipartisi berdasarkan kolom waktu, dan partisi ini disebut chunk. InfluxDB 3 Core adalah penulisan ulang generasi ketiga yang menyimpan data dalam bentuk kolumnar Parquet di atas object storage atau disk lokal, dengan antarmuka query SQL dan line protocol sebagai jalur ingestion utama. Keduanya dipilih pada jalur ini karena mewakili dua pendekatan berbeda, yaitu ekstensi pada basis data relasional matang versus basis data khusus yang dibangun dari nol untuk time-series.

Dataset contoh yang digunakan sepanjang jalur adalah telemetri sensor ruangan, dengan dua nilai terukur (suhu dan kelembapan) yang diambil setiap menit dari satu sensor selama satu hari penuh, yaitu 1.440 titik. Dataset ini cukup kecil untuk dimuat di memori, tetapi cukup besar untuk menunjukkan pola ingest, agregasi jendela waktu, dan downsampling. Skema pada kedua mesin akan memetakan dataset yang sama ke bentuk yang berbeda namun ekuivalen secara semantik.

Skema TimescaleDB sebagai hypertable#

Pada TimescaleDB, tabel biasa Postgres diciptakan terlebih dahulu, lalu dikonversi menjadi hypertable melalui fungsi create_hypertable. Kolom ts bertipe TIMESTAMPTZ menjadi poros waktu, dan chunk_time_interval menentukan rentang waktu per chunk. Satu hari adalah pilihan yang wajar untuk telemetri menitan, sebab seluruh dataset satu hari muat di satu chunk. Pilihan ini sekaligus menjadikan chunk sebagai unit retensi alami: ketika data satu hari sudah tidak diperlukan lagi, chunk itu dapat dihapus sepenuhnya tanpa pemindaian baris.

postgres Skema hypertable TimescaleDB
CREATE TABLE telemetri (
    ts             TIMESTAMPTZ NOT NULL,
    sensor_id      TEXT NOT NULL,
    suhu_c         DOUBLE PRECISION,
    kelembapan_pct DOUBLE PRECISION
);

SELECT create_hypertable(
    'telemetri',
    'ts',
    chunk_time_interval => INTERVAL '1 day'
);

CREATE INDEX telemetri_sensor_ts_idx
    ON telemetri (sensor_id, ts DESC);
text Keluaran
create_hypertable
(77,public,telemetri,t)

Indeks (sensor_id, ts DESC) adalah pola umum untuk query “suhu terbaru per sensor” maupun “rentang waktu tertentu untuk sensor tertentu”. Karena hypertable sudah mempartisi menurut ts, indeks ini bersifat lokal per chunk, sehingga ukurannya tidak meledak seiring pertumbuhan data. Pilihan TIMESTAMPTZ (timestamp with time zone) membuat kolom waktu disimpan dalam UTC dan ditampilkan sesuai zona waktu klien, yang merupakan praktik yang dianjurkan dokumentasi Postgres.

Skema InfluxDB sebagai line protocol#

InfluxDB 3 menggunakan model data measurement, tag, dan field. Measurement adalah nama deret (analog tabel), tag adalah kolom identitas yang diindeks (analog dimensi), dan field adalah nilai terukur. Pada dataset contoh, suhu dan kelembapan menjadi dua measurement terpisah, atau dapat juga disatukan dengan tag jenis. Line protocol adalah format teks baris-per-baris yang digunakan untuk ingestion. Berbeda dengan TimescaleDB yang mengharuskan CREATE TABLE lebih dulu, InfluxDB 3 tidak memerlukan pendefinisian skema eksplisit sebelum ingestion, sebab measurement dan field tercipta secara otomatis saat data pertama ditulis.

text Contoh line protocol InfluxDB
suhu,sensor_id=sensor-01 nilai=24.06 1735689600000000000
suhu,sensor_id=sensor-01 nilai=23.81 1735689660000000000
kelembapan,sensor_id=sensor-01 nilai=60.34i 1735689600000000000
kelembapan,sensor_id=sensor-01,lokasi=ruang-utama nilai=61.36 1735689660000000000

Baris terdiri atas tiga bagian yang dipisahkan spasi. Bagian pertama berisi measurement dan pasangan tag yang dipisahkan koma, bagian kedua berisi pasangan field yang juga dipisahkan koma, dan bagian ketiga (opsional) adalah cap waktu dalam nanodetik. Spasi adalah pemisah yang signifikan, dan tidak boleh ada spasi di sekitar tanda sama dengan. Sufiks i pada 60.34i menandakan tipe integer, sedangkan tanpa sufiks nilai dianggap float. Skema ini ringkas namun ketat, dan kesalahan kecil pada format dapat menyebabkan seluruh batch ditolak.

Membangkitkan telemetri contoh#

Dataset contoh dibangkitkan oleh skrip Python berikut, yang menggunakan pandas untuk membangun deret waktu dan numpy untuk menambahkan fluktuasi yang realistis. Skrip menulis dua berkas, yaitu telemetri.csv untuk konsumsi umum dan telemetri.json untuk konsumsi oleh skrip lain. Random seed ditetapkan secara eksplisit sehingga keluaran bersifat reproducible. Pemilihan satu sensor dengan interval satu menit selama satu hari menghasilkan 1.440 titik, yang merupakan jumlah cukup untuk menunjukkan pola harian tanpa membebani memori.

python telemetri_contoh.py
"""Bangkitkan telemetri sensor contoh untuk seluruh jalur time-series.

Skrip ini menghasilkan berkas telemetri.csv dan telemetri.json di direktori
yang sama dengan skrip. Dataset terdiri dari 1.440 titik (satu hari penuh,
interval satu menit) dengan dua nilai terukur: suhu dan kelembapan.
"""
from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path

import numpy as np
import pandas as pd

JUMLAH_TITIK = 1_440
FREKUENSI = "1min"
AWAL = datetime(2026, 1, 1, tzinfo=timezone.utc)
SENSOR_ID = "sensor-01"

def bangun() -> pd.DataFrame:
    cap_waktu = pd.date_range(AWAL, periods=JUMLAH_TITIK, freq=FREKUENSI, tz="UTC")
    rng = np.random.default_rng(42)
    fase_suhu = np.arange(JUMLAH_TITIK) / 60.0
    fase_lembap = np.arange(JUMLAH_TITIK) / 90.0
    suhu = 24.0 + np.sin(fase_suhu) + rng.normal(0, 0.2, JUMLAH_TITIK)
    kelembapan = 60.0 + np.cos(fase_lembap) + rng.normal(0, 1.0, JUMLAH_TITIK)
    return pd.DataFrame({
        "ts": cap_waktu,
        "sensor_id": SENSOR_ID,
        "suhu_c": np.round(suhu, 2),
        "kelembapan_pct": np.round(kelembapan, 2),
    })

def tulis_csv_json(df: pd.DataFrame, direktori: Path) -> None:
    df.to_csv(direktori / "telemetri.csv", index=False)
    df.to_json(direktori / "telemetri.json", orient="records", date_format="iso")

def main() -> None:
    sini = Path(__file__).resolve().parent
    df = bangun()
    tulis_csv_json(df, sini)
    print(f"jumlah baris: {len(df)}")
    print(f"rentang waktu: {df['ts'].min()} -> {df['ts'].max()}")
    print(f"sensor: {SENSOR_ID}")
    print("berkas ditulis: telemetri.csv, telemetri.json")

if __name__ == "__main__":
    main()
text Keluaran
jumlah baris: 1440
rentang waktu: 2026-01-01 00:00:00+00:00 -> 2026-01-01 23:59:00+00:00
sensor: sensor-01
berkas ditulis: telemetri.csv, telemetri.json

Berkas telemetri.csv berbentuk empat kolom, yaitu ts, sensor_id, suhu_c, dan kelembapan_pct. Berkas telemetri.json berbentuk array of objects dengan kunci yang sama. Kedua berkas ini menjadi sumber data kanonis untuk jalur ini. Pembaca dapat memilih format mana yang lebih sesuai untuk konsumsi: CSV untuk inspeksi cepat di terminal atau spreadsheet, dan JSON untuk konsumsi terprogram oleh skrip Python atau Node.js.

Membaca kembali dataset#

Pembacaan kembali penting untuk memverifikasi bahwa dataset yang ditulis dapat dibaca dengan benar. Skrip berikut membaca telemetri.csv, mencetak struktur kolom, rentang waktu, dan statistik ringkas dari kedua nilai terukur. Ringkasan ini juga berfungsi sebagai pemeriksaan cepat sebelum analisis lanjutan dilakukan. Pemeriksaan eksplisit terhadap jumlah baris, kolom, dan sensor unik membantu mendeteksi kesalahan pembangkitan sedini mungkin, misalnya apabila random seed berubah atau format CSV tidak konsisten.

python baca_telemetri.py
"""Baca telemetri.csv dan ringkas struktur datanya.

Skrip ini membaca berkas telemetri.csv yang dibangkitkan oleh
telemetri_contoh.py, lalu mencetak ringkasan struktur yang berguna
untuk verifikasi cepat sebelum analisis lanjutan.
"""
from pathlib import Path

import pandas as pd

def main() -> None:
    sini = Path(__file__).resolve().parent
    df = pd.read_csv(sini / "telemetri.csv", parse_dates=["ts"])
    print("jumlah baris:", len(df))
    print("kolom:", list(df.columns))
    print("rentang waktu:", df["ts"].min(), "->", df["ts"].max())
    print("sensor unik:", df["sensor_id"].unique().tolist())
    print("statistik ringkas:")
    print(df[["suhu_c", "kelembapan_pct"]].describe().round(2).to_string())

if __name__ == "__main__":
    main()
text Keluaran
jumlah baris: 1440
kolom: ['ts', 'sensor_id', 'suhu_c', 'kelembapan_pct']
rentang waktu: 2026-01-01 00:00:00+00:00 -> 2026-01-01 23:59:00+00:00
sensor unik: ['sensor-01']
statistik ringkas:
        suhu_c  kelembapan_pct
count  1440.00         1440.00
mean     24.02           59.94
std       0.73            1.24
min      22.48           55.95
25%      23.34           59.09
50%      24.08           59.97
75%      24.69           60.82
max      25.64           63.72

Statistik ringkas menunjukkan bahwa suhu bergerak di sekitar 24 derajat Celsius dengan deviasi sekitar 0,7, sedangkan kelembapan bergerak di sekitar 60 persen dengan deviasi sekitar 1,2. Distribusi yang sempit ini wajar untuk sensor ruangan dalam kondisi stabil, dan cocok untuk ilustrasi pola agregasi serta downsampling. Kuartil 25 persen dan 75 persen memberi gambaran rentang interkuartil yang lebih ringkas daripada nilai minimum dan maksimum, karena nilai ekstrem cenderung dikotori noise acak dari pembangkitan.

Parser line protocol di Node.js#

Karena line protocol adalah format teks, parsing dapat dilakukan tanpa klien resmi InfluxDB. Skrip Node.js berikut mengurai enam baris contoh, termasuk baris yang tidak memiliki cap waktu dan baris komentar yang diawali #. Logika parsing memisahkan baris menjadi tiga bagian berdasarkan spasi, lalu memecah bagian measurement/tag dan bagian field berdasarkan koma dan tanda sama dengan. Pendekatan ini juga merupakan cara yang digunakan klien resmi InfluxDB untuk memvalidasi baris sebelum mengirimnya ke server.

javascript parser_line_protocol.js
// Parser kecil untuk line protocol InfluxDB.
// Format baris: <measurement>,<tag>=<nilai>[,<tag>=<nilai>] <field>=<nilai>[,<field>=<nilai>] [<timestamp>]
// Spasi adalah pemisah yang signifikan; tidak boleh ada spasi di sekitar tanda sama dengan.

function parseLine(line) {
  const trimmed = line.trim();
  if (!trimmed || trimmed.startsWith("#")) return null;
  const parts = trimmed.split(" ");
  if (parts.length < 2) {
    throw new Error(`baris tidak lengkap: ${line}`);
  }
  let timestamp = null;
  let fieldPart;
  if (parts.length >= 3 && /^\d+$/.test(parts[parts.length - 1])) {
    timestamp = parts[parts.length - 1];
    fieldPart = parts.slice(1, -1).join(" ");
  } else {
    fieldPart = parts.slice(1).join(" ");
  }
  const measurementTags = parts[0];
  const [measurement, ...tagPairs] = measurementTags.split(",");
  const tags = {};
  for (const pair of tagPairs) {
    const [k, v] = pair.split("=");
    tags[k] = v;
  }
  const fields = {};
  for (const pair of fieldPart.split(",")) {
    const [k, v] = pair.split("=");
    fields[k] = v;
  }
  return { measurement, tags, fields, timestamp };
}

const contoh = [
  "suhu,sensor_id=sensor-01 nilai=24.06 1735689600000000000",
  "suhu,sensor_id=sensor-01 nilai=23.81 1735689660000000000",
  "kelembapan,sensor_id=sensor-01 nilai=60.34i 1735689600000000000",
  "kelembapan,sensor_id=sensor-01,lokasi=ruang-utama nilai=61.36 1735689660000000000",
  "# baris komentar diabaikan",
  "suhu,sensor_id=sensor-01 nilai=24.18",
];

for (const line of contoh) {
  try {
    const hasil = parseLine(line);
    if (hasil) {
      console.log(JSON.stringify(hasil));
    }
  } catch (err) {
    console.error("error:", err.message);
  }
}
text Keluaran
{"measurement":"suhu","tags":{"sensor_id":"sensor-01"},"fields":{"nilai":"24.06"},"timestamp":"1735689600000000000"}
{"measurement":"suhu","tags":{"sensor_id":"sensor-01"},"fields":{"nilai":"23.81"},"timestamp":"1735689660000000000"}
{"measurement":"kelembapan","tags":{"sensor_id":"sensor-01"},"fields":{"nilai":"60.34i"},"timestamp":"1735689600000000000"}
{"measurement":"kelembapan","tags":{"sensor_id":"sensor-01","lokasi":"ruang-utama"},"fields":{"nilai":"61.36"},"timestamp":"1735689660000000000"}
{"measurement":"suhu","tags":{"sensor_id":"sensor-01"},"fields":{"nilai":"24.18"},"timestamp":null}

Keluaran di atas menunjukkan tiga hal penting. Pertama, baris dengan tag tambahan lokasi=ruang-utama tetap terurai dengan benar karena tag dipisahkan koma. Kedua, baris tanpa cap waktu menghasilkan timestamp: null, yang berarti InfluxDB akan mengisi cap waktu dengan waktu server saat ingestion. Ketiga, baris komentar yang diawali # diabaikan tanpa error, sehingga berkas line protocol dapat memuat anotasi yang tidak ikut diingest. Parser ini cukup untuk verifikasi format sebelum pengiriman batch ke server InfluxDB.

Kesalahan umum#

  • Menjalankan container tanpa volume persisten. Data hilang saat container dimatikan, dan seluruh dataset contoh harus dibangkitkan ulang.
  • Mencampur tipe data pada field yang sama. Line protocol tidak menggunakan skema kaku, tetapi mengubah tipe field dari float ke integer di tengah dataset dapat menyebabkan query agregasi gagal.
  • Memilih chunk_time_interval yang terlalu kecil. Chunk per menit pada telemetri menitan menghasilkan terlalu banyak chunk untuk sedikit data, sehingga overhead manajemen chunk melebihi manfaat partisi.

Ringkasan#

  • TimescaleDB dan InfluxDB 3 Core mewakili dua pendekatan berbeda untuk basis data time-series.
  • Dataset contoh berupa 1.440 titik telemetri sensor, ditulis sebagai CSV dan JSON oleh skrip Python.
  • Skema hypertable menggunakan tabel Postgres biasa yang dipartisi otomatis menurut waktu.
  • Line protocol InfluxDB adalah format teks baris-per-baris dengan measurement, tag, field, dan cap waktu.
  • Parser line protocol dapat dibangun di Node.js tanpa klien resmi, karena formatnya bersifat teks.

Referensi#