Data time-series adalah rekaman nilai yang dipasangkan dengan cap waktu (timestamp) dan disusun menurut kronologi. Topik ini membedah tiga sifat khas yang membedakan data time-series dari data operasional biasa, yaitu dominasi penambahan, urutan waktu yang menjadi bagian skema, dan pertumbuhan tanpa batas. Setelah membaca topik ini, akar ketegangan antara data time-series dan basis data berbasis baris (row store) menjadi jelas, lengkap dengan ilustrasi laju pertumbuhan yang dapat dihitung.
Data, cap waktu, dan urutan#
Pada basis data relasional biasa, baris menyatakan sebuah peristiwa atau entitas, misalnya satu pengguna, satu pesanan, atau satu catatan stok. Pada data time-series, baris menyatakan satu titik pengukuran (sample) dari deret yang berkesinambungan. Setiap titik membawa minimal dua hal, yaitu cap waktu dan nilai terukur, serta sering kali label identitas seperti sensor_id atau host. Susunan ini membuat baris tidak berdiri sendiri secara semantik, sebab titik sebelum dan sesudahnya membentuk konteks yang diperlukan untuk menafsirkan nilai titik tersebut.
Kontrak urutan menjadi bagian dari skema, bukan urusan presentasi. Sebuah query yang meminta “suhu rata-rata selama satu jam terakhir” tidak mungkin dijawab tanpa jaminan bahwa baris telah diindeks dan disusun menurut cap waktu. Berbeda dengan kolom nama atau email yang hanya tempat penyimpanan nilai, kolom cap waktu menjadi poros partisi, poros indeks, dan poros retensi sekaligus. Karena itulah mesin time-series memperlakukan cap waktu sebagai warga kelas satu, bukan sekadar kolom TIMESTAMP biasa.
Didominasi penambahan#
Pola penulisan time-series nyaris selalu berbentuk penambahan (append-only). Baris baru terus dimasukkan di ujung termutakhir, sedangkan baris lama hampir tidak pernah diubah. Sifat ini disebut append-mostly, dan implikasinya cukup besar pada pemilihan struktur penyimpanan. Pada tabel transaksi seperti pesanan, perubahan status, pembatalan, dan koreksi membuat baris kerap diperbarui. Pada tabel telemetri, koreksi terjadi sangat jarang sehingga model immutable batch menjadi sangat cocok.
Konsekuensi praktis dari sifat ini adalah struktur penyimpanan dapat dioptimalkan untuk aliran data searah. Halaman penyimpanan baru dapat dialokasikan di akhir tanpa memerlukan pencarian ruang kosong di tengah tabel. Indeks sekunder pada kolom identitas seperti sensor_id dapat menggunakan pola penambahan batch, bukan pola pembaruan acak. Kompresi kolumnar menjadi sangat efektif karena baris lama membeku dan pola nilai di dalamnya tidak berubah. Mesin time-series memanfaatkan sifat ini untuk mengompresi chunk lama ke format kolumnar, sehingga ukuran disk mengecil berkali-kali lipat tanpa kehilangan kemampuan query.
Terurut waktu, tumbuh tanpa batas#
Cap waktu yang terurut adalah poros yang mengikat seluruh beban. Karena penambahan selalu di ujung termutakhir, dan ujung termutakhir selalu memiliki cap waktu terbesar, maka secara alami baris tersusun menurut waktu. Sifat ini memungkinkan partisi berbasis waktu bekerja sangat baik: setiap partisi (chunk pada TimescaleDB, shard pada InfluxDB 3.x) menyimpan rentang waktu tertentu, dan query rentang waktu hanya menyentuh sebagian kecil partisi yang aktif.
Sifat kedua yang berkaitan adalah pertumbuhan tanpa batas. Tidak ada batas atas bawaan pada volume data time-series, sebab pengukuran baru terus datang selama perangkat atau layanan masih berjalan. Karakter ini berbeda dari tabel master seperti pengguna yang pertumbuhannya melambat seiring waktu. Laju ingest satu titik per detik sudah menghasilkan lebih dari 31 juta baris per tahun, dan sepuluh ribu sensor dengan frekuensi satu titik per detik menghasilkan lebih dari 300 miliar baris per tahun. Skala seperti ini bukan skenario tepi, melainkan target operasional biasa.
Beban pada row store#
Row store menyimpan baris demi baris dalam halaman berukuran 8 KB pada Postgres atau 16 KB pada MySQL. Setiap halaman berisi beberapa baris, dan satu baris panjang dapat berada di beberapa halaman. Struktur ini sangat baik untuk beban operasional yang membaca dan menulis satu baris penuh sekaligus. Namun, untuk data time-series, struktur ini menimbulkan tiga beban yang saling menumpuk.
Beban pertama adalah fragmentasi halaman karena penambahan terus-menerus. Halaman baru dialokasikan di akhir tabel, dan vacuum secara berkala memadatkan kembali ruang yang ditinggalkan baris yang sudah dihapus. Pada tabel besar dengan laju ingest tinggi, biaya vacuum menjadi signifikan. Beban kedua adalah amplifikasi penulisan indeks. Setiap kolom yang diindeks, terutama indeks B-tree pada sensor_id, harus diperbarui setiap kali baris baru ditambahkan. Indeks B-tree pada Postgres menyimpan kunci dan tuple identifier di halaman terpisah, sehingga satu penambahan baris memicu penulisan ke beberapa halaman indeks sekaligus.
Beban ketiga adalah retensi yang mahal. Menghapus baris lama pada tabel biasa berarti menjalankan DELETE ... WHERE ts < ..., yang memicu pemindaian dan menandai baris sebagai dihapus, lalu menunggu vacuum untuk memadatkan kembali. Pada tabel raksasa, perintah DELETE ini menjadi sangat mahal dan mengunci halaman dalam waktu lama. Karena biaya DELETE yang besar, mesin time-series memilih partisi berbasis waktu sebagai unit retensi: menghapus chunk lama hanya berarti melepaskan seluruh berkas partisi, bukan memindai baris demi baris.
Mengukur laju pertumbuhan#
Sifat tumbuh tanpa batas paling mudah dipahami melalui angka. Banyak arsitek yang tertipu oleh angka harian yang tampak kecil, lalu terkejut ketika akumulasi tahunan muncul di kapasitas disk. Perhitungan sederhana berikut menghitung berapa baris dihasilkan dalam sehari, sebulan (30 hari), dan setahun untuk beberapa laju ingest yang lazim, dari satu titik per detik hingga seribu titik per detik. Angka-angka ini tidak bersifat hipotetis, sebab laju seratus titik per detik adalah nominal biasa untuk pemantauan satu host, dan seribu titik per detik adalah nominal wajar untuk satu kluster kecil.
"""Proyeksi pertumbuhan baris pada beban time-series.
Skrip ini menghitung berapa baris dihasilkan dalam sehari, sebulan (30 hari),
dan setahun untuk beberapa laju ingest yang lazim. Tujuannya menunjukkan sifat
tumbuh tanpa batas yang dimiliki data time-series.
"""
DETIK_PER_HARI = 24 * 60 * 60
LAJU_PER_DETIK = [1, 10, 100, 1_000]
def baris_dalam(hari: int, laju: int) -> int:
return laju * DETIK_PER_HARI * hari
def format_angka(n: int) -> str:
return f"{n:,}".replace(",", ".")
def cetak_tabel() -> None:
header = f"{'laju/detik':>10} {'per_hari':>14} {'per_bulan':>16} {'per_tahun':>18}"
print(header)
print("-" * len(header))
for laju in LAJU_PER_DETIK:
per_hari = baris_dalam(1, laju)
per_bulan = baris_dalam(30, laju)
per_tahun = baris_dalam(365, laju)
print(f"{laju:>10} {format_angka(per_hari):>14} {format_angka(per_bulan):>16} {format_angka(per_tahun):>18}")
if __name__ == "__main__":
cetak_tabel()laju/detik per_hari per_bulan per_tahun
-------------------------------------------------------------
1 86.400 2.592.000 31.536.000
10 864.000 25.920.000 315.360.000
100 8.640.000 259.200.000 3.153.600.000
1000 86.400.000 2.592.000.000 31.536.000.000Baris terendah pada tabel di atas menunjukkan angka yang penting untuk diingat. Laju seribu titik per detik menghasilkan 86 juta baris per hari, dan dalam satu tahun akumulasi mencapai 31 miliar baris. Pada skala ini, perbedaan antara menghapus seluruh chunk dan menjalankan DELETE baris-per-baris menjadi perbedaan antara hitungan menit dan hitungan jam. Perbedaan tersebut menentukan apakah retensi harian bisa berjalan tanpa mengganggu beban utama.
Kesalahan umum#
- Menganggap kolom
TIMESTAMPsaja sudah cukup untuk menjadikan sebuah tabel sebagai time-series. Indeks B-tree padatstidak mengubah cara penyimpanan, sebab tabel tetap menyimpan baris dalam halaman 8 KB dan tetap dibebani vacuum. - Menggunakan satu tabel raksasa tanpa partisi. Tanpa partisi waktu, perintah
DELETEuntuk retensi memerlukan pemindaian penuh, dan ukuran indeks tumbuh tanpa hingga. - Mengabaikan amplifikasi penulisan indeks. Banyaknya indeks pada satu tabel berarti banyak halaman yang harus ditulis ulang untuk setiap baris baru, sehingga jumlah indeks perlu diperhitungkan sebelum skema dibekukan.
Ringkasan#
- Data time-series adalah titik pengukuran berurutan waktu, dan urutan menjadi kontrak, bukan presentasi.
- Tiga sifat khasnya adalah append-mostly, terurut waktu, dan tumbuh tanpa batas.
- Row store dibebani tiga hal, yaitu fragmentasi halaman, amplifikasi indeks, dan retensi baris-per-baris.
- Partisi berbasis waktu menjadi jawaban mesin time-series terhadap sifat pertumbuhan tanpa batas.
Referensi#
- Hypertables — Timescale — Penjelasan resmi tentang hypertable sebagai abstraksi tabel yang dipartisi otomatis berdasarkan waktu.
- Database Page Storage — PostgreSQL — Tata letak halaman 8 KB pada Postgres yang menjelaskan biaya fragmentasi dan vacuum.
- Table Partitioning — PostgreSQL — Mekanisme partisi deklaratif yang menjadi dasar hypertable TimescaleDB.