Kompresi kolumnar pada TimescaleDB mengubah penyimpanan chunk lama dari baris-ke-baris menjadi kolom-per-kolom dengan encoding bertipe, sehingga data redundan pada telemetri menyusut drastis tanpa mengubah SQL aplikasi. Topik ini menjelaskan transisi dari row store ke columnar store, mekanisme encoding yang digunakan TimescaleDB, cara mengaktifkan kompresi melalui ALTER TABLE ... SET (timescaledb.compress, ...), serta kebijakan add_compression_policy yang mengotomatiskan transisi berdasarkan usia chunk. Setelah membaca topik ini, sebuah hypertable yang tumbuh tanpa batas dapat dilengkapi kebijakan kompresi yang menjaga ukuran disk tetap terkendali seiring bertambahnya usia data.
Penyimpanan baris vs kolom#
Pada Postgres biasa, data disimpan secara baris-per-baris dalam heap tuple. Setiap baris memuat seluruh kolomnya, mulai dari header tuple hingga nilai terakhir, sehingga satu baris telemetri dengan tiga kolom memerlukan sekitar 46 byte termasuk overhead. Tata letak ini efisien untuk beban online transaction processing, sebab seluruh baris dapat dibaca dalam satu sequential scan pada disk. Pada beban time-series, pola aksesnya berbeda, sebab query biasanya hanya menyentuh beberapa kolom tertentu, misalnya ts dan suhu_c, sedangkan kolom lain tidak dibaca sama sekali. Akibatnya, row store tetap membaca seluruh baris dari disk, sehingga banyak byte yang dibaca tetapi tidak terpakai.
Penyimpanan kolumnar menyusun ulang data per kolom, bukan per baris. Seluruh nilai kolom ts diletakkan berdekatan, demikian pula seluruh nilai sensor_id dan suhu_c. Tata letak ini memberi dua keuntungan. Pertama, query yang hanya menyentuh beberapa kolom hanya membaca kolom yang relevan, sehingga disk I/O turun secara proporsional. Kedua, kolom dengan nilai yang berdekat atau berulang menjadi kandidat kompresi yang sangat baik. Cap waktu yang konstan tiap menit dapat dikodekan sebagai delta-of-delta, identitas seri yang panjang run-nya dapat dikodekan sebagai run-length encoding, dan nilai terukur yang berubah lambat dapat dikodekan dengan XOR berturut-turut ala Gorilla.
Mekanisme encoding TimescaleDB#
TimescaleDB menggunakan encoding yang sebagian besar meniru Gorilla, yaitu algoritma kompresi yang dikembangkan oleh Facebook untuk telemetri infrastruktur. Pada cap waktu, delta-of-delta mengkodekan perubahan terhadap perubahan sebelumnya, sehingga rentang dengan interval konstan, misalnya tiap 60 detik, menghasilkan nilai nol yang sangat kompak. Pada identitas seri, run-length encoding mengkodekan rentetan nilai yang sama sebagai pasangan nilai dan jumlah, sehingga kolom sensor_id yang diurutkan per sensor menghasilkan hanya beberapa run untuk seluruh chunk. Pada nilai terukur, XOR-based encoding memanfaatkan fakta bahwa nilai bertipe numerik yang berdekatan memiliki banyak bit awal dan akhir yang sama, sehingga selisihnya pendek.
Kombinasi ketiga encoding itu, ditambah dengan segmentby yang mengelompokkan baris berdasarkan nilai kolom tertentu, menghasilkan rasio kompresi yang umumnya berkisar antara 8 hingga 20 kali pada telemetri nyata. Rasio ini bergantung pada keteraturan data, sebab data yang diurutkan per sensor dan per waktu memberi run lebih panjang dan delta lebih konstan. Rasio yang lebih rendah biasanya terjadi pada kolom dengan kardinalitas tinggi atau nilai yang berubah cepat, sedangkan rasio yang lebih tinggi terjadi pada kolom dengan banyak pengulangan atau nilai konstan.
Mengaktifkan kompresi pada hypertable#
Kompresi pada TimescaleDB diaktifkan per hypertable dengan ALTER TABLE ... SET (timescaledb.compress, ...). Dua parameter utama menentukan efektivitasnya. Parameter compress_segmentby menentukan kolom yang digunakan untuk mengelompokkan baris sebelum dikodekan, biasanya identitas seri seperti sensor_id, sehingga seluruh titik dari satu sensor berdekatan dan run-length menjadi optimal. Parameter compress_orderby menentukan urutan baris di dalam setiap kelompok segmentby, biasanya ts DESC, sehingga cap waktu berurutan dan delta-of-delta menjadi konstan. Pemilihan kedua parameter ini menentukan rasio kompresi yang akhirnya diperoleh. Perintah berikut tidak dijalankan di sandbox karena memerlukan Postgres dengan TimescaleDB aktif, sehingga hanya menjadi referensi sintaksis.
-- hypertable 'suhu_sensor' sudah ada dan berisi telemetri menitan
ALTER TABLE suhu_sensor SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id',
timescaledb.compress_orderby = 'ts DESC'
);
-- kompresi satu chunk lama secara manual
SELECT compress_chunk(format('%I.%I', chunk_schema, chunk_name))
FROM timescaledb_information.chunks
WHERE hypertable_name = 'suhu_sensor'
ORDER BY range_end DESC
LIMIT 1;
-- lihat statistik kompresi per chunk
SELECT chunk_name,
before_compression_total_bytes,
after_compression_total_bytes
FROM chunk_compression_stats('suhu_sensor');compress_chunk
_timescaledb_internal._hyper_87_174_chunk
chunk_name|before_compression_total_bytes|after_compression_total_bytes
_hyper_87_175_chunk||
_hyper_87_176_chunk||
_hyper_87_174_chunk|434176|114688Kebijakan kompresi otomatis#
Aktivasi kompresi secara manual pada setiap chunk lama tidak praktis pada hypertable yang tumbuh harian. TimescaleDB menyediakan add_compression_policy yang mengotomatiskan transisi dari row store ke columnar store berdasarkan usia chunk. Argumen compress_after menentukan kapan sebuah chunk menjadi kandidat kompresi, biasanya INTERVAL '7 days', sehingga chunk berusia lebih dari tujuh hari dikompresi oleh pekerjaan latar belakang. Pemilihan compress_after harus mempertimbangkan pola late arrival, sebab kompresi pada chunk yang masih menerima baris baru memerlukan dekompresi sebelum insert, yang menjadi operasi yang mahal.
Setelah kebijakan aktif, pekerjaan latar belakang TimescaleDB memeriksa hypertable secara berkala dan mengompresi chunk yang memenuhi kriteria usia. Pemantauan kebijakan ini dapat dilakukan melalui tampilan timescaledb_information.jobs dan chunk_compression_stats, yang masing-masing menampilkan jadwal pekerjaan dan rasio kompresi per chunk. Perintah berikut tidak dijalankan di sandbox dan hanya menjadi referensi sintaksis.
SELECT add_compression_policy(
'suhu_sensor',
compress_after => INTERVAL '7 days'
);
-- daftar pekerjaan kompresi terdaftar
SELECT job_id, application_name, schedule_interval, config
FROM timescaledb_information.jobs
WHERE proc_name = 'policy_compression';add_compression_policy
1014
job_id|application_name|schedule_interval|config
1014|Compression Policy [1014]|12:00:00|{"hypertable_id": 87, "compress_after": "7 days"}Simulasi rasio kompresi#
Karena kompresi TimescaleDB pada dasarnya adalah delta-of-delta pada cap waktu, run-length pada identitas seri, dan XOR pada nilai terukur, skrip Python berikut memodelkan ketiganya dengan pendekatan kasar. Skrip membangkitkan satu minggu telemetri dengan tiga sensor dan interval satu menit, lalu menghitung ukuran row store dan ukuran kolumnar. Encoding di sini adalah penyederhanaan dari Gorilla yang sebenarnya, sehingga rasio yang dihasilkan bersifat ilustratif, bukan pengukuran disk. Skrip tidak menjalankan SQL apa pun.
"""Simulasi rasio kompresi kolumnar pada TimescaleDB.
Skrip ini tidak menjalankan SQL. Tujuannya memperlihatkan mengapa kompresi
kolumnar pada chunk lama TimescaleDB memberikan rasio kompresi yang tinggi,
dengan membandingkan ukuran penyimpanan baris-ke-baris (row store) terhadap
penyimpanan kolom-per-kolom dengan encoding sederhana (delta-of-delta untuk
cap waktu, run-length untuk sensor_id, dan XOR untuk nilai terukur).
Encoding di sini adalah penyederhanaan dari algoritma Gorilla yang dipakai
TimescaleDB. Tujuannya bukan mereproduksi rasio disk sebenarnya, melainkan
memperlihatkan dua hal: data redundan menghasilkan rasio kompresi tinggi,
dan kolom dengan kardinalitas berbeda memberi kontribusi yang berbeda.
"""
from __future__ import annotations
import pandas as pd
AWAL = "2026-11-01 00:00:00"
TITIK = 10_080 # 7 hari x 1.440 titik
SENSORS = ["S01", "S02", "S03"]
def bangun_telemetri() -> pd.DataFrame:
ts = pd.date_range(AWAL, periods=TITIK, freq="1min", tz="UTC")
baris = []
for s in SENSORS:
for i, t in enumerate(ts):
suhu = 22.0 + (t.hour - 12) * 0.05 + (hash(s) % 7) * 0.1
baris.append((t, s, round(suhu, 2)))
return pd.DataFrame(baris, columns=["ts", "sensor_id", "suhu_c"])
def ukuran_row_store(df: pd.DataFrame) -> int:
# Postgres heap tuple: header 24 byte + timestamptz 8 + text sensor_id 6 +
# double 8 = 46 byte per baris (angka kasar tanpa pad to alignment).
return len(df) * (24 + 8 + 6 + 8)
def encode_delta_of_delta(ts_ser: pd.Series) -> int:
# delta-of-delta: konstanta 60 detik hampir sepanjang seri, sehingga
# sebagian besar nilai menjadi 0 dan hanya butuh 1 bit per titik pada
# implementasi Gorilla. Pendekatan kasar: 1 byte per titik.
return len(ts_ser) * 1
def encode_run_length(sensor_ser: pd.Series) -> int:
# RLE: tiap run disimpan sebagai (nilai, jumlah). 3 sensor berselang-seling
# per menit -> 10.080 run, tetapi bila diurutkan per sensor terlebih dahulu
# hanya 3 run.
runs = sensor_ser.ne(sensor_ser.shift()).cumsum().nunique()
return runs * (6 + 4) # 6 byte string + 4 byte count
def encode_xor(suhu_ser: pd.Series) -> int:
# XOR berturut-turut pada nilai suhu yang berubah lambat sebagian besar
# menghasilkan leading zero / trailing zero yang tinggi, sehingga 1-2 byte
# per titik adalah perkiraan kasar.
return len(suhu_ser) * 2
def ukuran_columnar(df: pd.DataFrame) -> int:
# urutkan per sensor untuk memaksimalkan RLE pada sensor_id dan
# keteraturan delta pada ts
df = df.sort_values(["sensor_id", "ts"]).reset_index(drop=True)
ts_b = encode_delta_of_delta(df["ts"])
sensor_b = encode_run_length(df["sensor_id"])
suhu_b = encode_xor(df["suhu_c"])
metadata = 64 # ringkasan statistik per kolom, sangat kasar
return ts_b + sensor_b + suhu_b + metadata
def main() -> None:
df = bangun_telemetri()
print(f"chunk hipotetis: {len(df)} baris ({TITIK} titik x {len(SENSORS)} sensor)")
print(f"rentang: {df['ts'].min()} -> {df['ts'].max()}")
print()
row_b = ukuran_row_store(df)
col_b = ukuran_columnar(df)
print(f"ukuran row store (heap tuple): {row_b:>10} byte ({row_b/1024:.1f} KB)")
print(f"ukuran columnar (delta+RLE+XOR): {col_b:>10} byte ({col_b/1024:.1f} KB)")
print(f"rasio kompresi: {row_b/col_b:.1f}x")
print()
df_sorted = df.sort_values(["sensor_id", "ts"]).reset_index(drop=True)
print("kontribusi per kolom (setelah diurutkan per sensor):")
print(f" ts (delta-of-delta): {encode_delta_of_delta(df_sorted['ts']):>8} byte")
print(f" sensor_id (RLE): {encode_run_length(df_sorted['sensor_id']):>8} byte")
print(f" suhu_c (XOR): {encode_xor(df_sorted['suhu_c']):>8} byte")
print()
print("padanan SQL TimescaleDB:")
print(" ALTER TABLE suhu_sensor SET (")
print(" timescaledb.compress,")
print(" timescaledb.compress_segmentby = 'sensor_id',")
print(" timescaledb.compress_orderby = 'ts DESC'")
print(" );")
print(" SELECT add_compression_policy('suhu_sensor',")
print(" compress_after => INTERVAL '7 days');")
if __name__ == "__main__":
main()chunk hipotetis: 30240 baris (10080 titik x 3 sensor)
rentang: 2026-11-01 00:00:00+00:00 -> 2026-11-07 23:59:00+00:00
ukuran row store (heap tuple): 1391040 byte (1358.4 KB)
ukuran columnar (delta+RLE+XOR): 90814 byte (88.7 KB)
rasio kompresi: 15.3x
kontribusi per kolom (setelah diurutkan per sensor):
ts (delta-of-delta): 30240 byte
sensor_id (RLE): 30 byte
suhu_c (XOR): 60480 byte
padanan SQL TimescaleDB:
ALTER TABLE suhu_sensor SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'sensor_id',
timescaledb.compress_orderby = 'ts DESC'
);
SELECT add_compression_policy('suhu_sensor',
compress_after => INTERVAL '7 days');Keluaran di atas memperlihatkan tiga hal. Pertama, rasio kompresi 15,3 kali yang diperoleh dari model sederhana berada dalam kisaran 8 hingga 20 kali yang umum dilaporkan pada telemetri nyata. Kedua, kontribusi terbesar pada ukuran kolumnar berasal dari kolom suhu_c, sebab nilai terukur berubah setiap titik dan XOR memberi rasio paling rendah di antara ketiga encoding. Ketiga, kontribusi sensor_id menjadi sangat kecil setelah pengurutan, sebab seluruh 10.080 titik per sensor menjadi satu run dan hanya butuh tiga pasangan nilai dan jumlah. Diagram berikut memvisualisasikan transisi dari row store ke columnar store.
Transisi baris ke kolom dalam praktik#
Transisi dari row store ke columnar store tidak mengubah nama tabel, kolom, atau tipe data, sehingga query SQL tetap ditulis seperti biasa. Yang berubah adalah cara Postgres membaca data, sebab chunk dispatch hook TimescaleDB mendeteksi bahwa chunk yang dipindai sudah dikompresi dan menjalankan dekompresi yang sesuai. Untuk query yang hanya menyentuh beberapa kolom, hanya kolom itu yang didekompresi, sehingga disk I/O tetap rendah. Untuk query yang menyentuh seluruh kolom, dekompresi penuh terjadi, tetapi tetap lebih cepat daripada row store karena locality kolom yang lebih baik.
Penulisan ulang ke chunk yang sudah dikompresi memerlukan dekompresi sebelumnya, sehingga operasi INSERT, UPDATE, dan DELETE pada chunk terkompresi menjadi lebih mahal daripada pada chunk panas. Hal ini menjelaskan mengapa compress_after biasanya diatur beberapa hari ke belakang, sebab chunk yang masih menerima baris baru sebaiknya tetap dalam bentuk row store. Bila late arrival datang ke chunk yang sudah dikompresi, TimescaleDB secara otomatis mendekompresi chunk itu sebelum insert, lalu menjadwalkan kompresi ulang pada iterasi berikutnya. Biaya ini menjadikan kompresi sebagai keputusan tradeoff, bukan optimasi gratis.
Kesalahan umum#
- Memilih
compress_segmentbypada kolom dengan kardinalitas tinggi, misalnya ID transaksi unik. Setiap nilai baru membentuk segmen sendiri, sehingga run-length tidak efektif dan rasio kompresi jatuh. - Lupa menambahkan
compress_orderbypada kolom waktu. Tanpa pengurutan, delta-of-delta pada cap waktu tidak konstan, sehingga encoding menjadi jauh lebih boros. - Mengaktifkan kompresi pada seluruh hypertable tanpa kebijakan usia. Akibatnya chunk panas yang masih menerima insert menjadi sering dikompresi dan didekompresi, sehingga latensi ingest naik tanpa manfaat kompresi yang signifikan.
Ringkasan#
- Kompresi TimescaleDB mengubah penyimpanan chunk dari baris-per-baris ke kolom-per-kolom dengan encoding delta-of-delta, run-length, dan XOR ala Gorilla.
compress_segmentbydancompress_orderbymenentukan rasio kompresi, dengan identitas seri sebagai segmentby dan cap waktu sebagai orderby yang umum.- Kebijakan
add_compression_policymengotomatiskan transisi berdasarkan usia chunk, sehingga chunk panas tetap dalam bentuk row store dan chunk lama dikompresi tanpa intervensi manual.
Referensi#
- Compression — Timescale
— Penjelasan resmi kompresi kolumnar TimescaleDB, termasuk
compress_segmentby,compress_orderby, dan kebijakan otomatis. - add_compression_policy — Timescale
— Referensi resmi fungsi
add_compression_policybeserta argumencompress_afterdan jadwalnya. - Gorilla: A Fast, Scalable, In-Memory Time Series Database — Facebook/VLDB — Makalah asli yang memperkenalkan encoding delta-of-delta dan XOR yang diadopsi TimescaleDB.