Prakomputasi vs saat query

Continuous aggregates berhadapan dengan agregasi ad-hoc, disertai perhitungan biaya.

Daftar isi

Agregasi atas data time-series dapat dihitung saat query diterima atau diprakomputasi sebelumnya dan disimpan pada objek termaterialisasi. Topik ini menjelaskan perbedaan keduanya pada sisi pemindaian, perhitungan biaya baris yang dipindai pada beban dashboard, biaya refresh yang memindahkan beban ke latar belakang, serta peran continuous aggregates TimescaleDB sebagai wadah prakomputasi yang menyegarkan diri. Setelah membaca topik ini, keputusan antara ad-hoc dan prakomputasi dapat diambil dengan dasar perhitungan, bukan sekadar preferensi.

Ad-hoc vs prakomputasi: perbedaan sumber pemindaian#

Agregasi ad-hoc adalah query yang membaca hypertable mentah setiap kali diterima, sehingga pekerjaan agregasi diulang pada setiap query. Bila seratus dashboard meminta “rerata harian tujuh hari terakhir” pada saat yang bersamaan, basis data memproses seratus pemindaian yang setara, masing-masing menyentuh 10.080 baris per sensor. Pola ini berlebihan, sebab jawaban yang diharapkan tidak berubah selama rentang waktu yang diproses belum menerima titik baru. Prakomputasi menyingkirkan kerja berlebihan tersebut dengan menghitung agregat sekali, lalu menyimpannya sebagai continuous aggregate TimescaleDB. Query yang datang setelahnya membaca continuous aggregate, bukan hypertable sumber, sehingga jumlah baris yang dipindai turun drastis.

Perbedaan utama antara kedua pendekatan terletak pada sumber pemindaian. Pada ad-hoc, sumber pemindaian adalah hypertable mentah yang memuat semua titik per menit, sedangkan pada prakomputasi, sumber pemindaian adalah continuous aggregate yang sudah memuat agregat per bucket yang lebih besar. Sebuah continuous aggregate harian, misalnya, memuat satu baris per hari per sensor, sehingga query “rerata harian tujuh hari terakhir” hanya memindai tujuh baris, bukan 10.080 baris. Selisih ini bukan sekadar optimasi kecil, sebab beban kumulatif pada dashboard yang sibuk dapat mencapai ratusan ribu pemindaian per jam.

Perhitungan biaya: baris dipindai per query#

Skrip berikut mengestimasi jumlah baris yang dipindai untuk query dashboard yang sama, yaitu rerata harian tujuh hari terakhir, pada tiga sumber data. Estimasi dilakukan dengan asumsi sederhana, yaitu satu titik per menit per sensor, sehingga tujuh hari setara dengan 10.080 baris pada hypertable mentah, 168 baris pada continuous aggregate per jam, dan tujuh baris pada continuous aggregate harian. Skrip juga menghitung biaya kumulatif per jam untuk 100 sensor yang masing-masing menjalankan 10 query per menit, sehingga perhitungan biaya menjadi konkret.

python biaya_prakomputasi.py
"""Demonstrasi prakomputasi vs agregasi ad-hoc: perhitungan biaya baris dipindai.

Skrip mengestimasi jumlah baris yang dipindai untuk query dashboard yang
sama, yaitu rerata harian 7 hari terakhir, pada tiga sumber data, yaitu
hypertable mentah, continuous aggregate per jam, dan continuous aggregate
harian. Skrip juga menghitung biaya kumulatif per jam untuk 100 sensor
yang masing-masing menjalankan 10 query per menit, serta biaya refresh
continuous aggregate harian. Tujuannya memperlihatkan bahwa prakomputasi
memindahkan beban dari sisi query ke sisi refresh yang jauh lebih jarang.
"""
from __future__ import annotations

import pandas as pd

HARI_MENTAH = 30
TITIK_PER_HARI = 1440  # 1 titik/menit


def estimasi(rentang_hari: int, sumber: str) -> dict:
    """Estimasi kasar baris yang dipindai untuk query agregasi 7 hari terakhir."""
    if sumber == "raw":
        baris = rentang_hari * TITIK_PER_HARI
        label = "hypertable mentah"
    elif sumber == "cagg_harian":
        baris = rentang_hari
        label = "cagg harian"
    elif sumber == "cagg_jam":
        baris = rentang_hari * 24
        label = "cagg per jam"
    else:
        baris = 0
        label = "?"
    return {"label": label, "baris_dipindai": baris}


def main() -> None:
    print("skenario: 1 sensor, 1 titik/menit, 30 hari data mentah")
    print(f"total baris mentah: {HARI_MENTAH * TITIK_PER_HARI}")
    print()

    rentang = 7
    print(f"query dashboard: rerata harian {rentang} hari terakhir")
    skenario = [
        ("ad-hoc (raw)", estimasi(rentang, "raw")),
        ("cagg per jam", estimasi(rentang, "cagg_jam")),
        ("cagg harian", estimasi(rentang, "cagg_harian")),
    ]
    baris = []
    for label, info in skenario:
        baris.append({
            "skenario": label,
            "sumber": info["label"],
            "baris_dipindai": info["baris_dipindai"],
        })
    df = pd.DataFrame(baris)
    df["rasio_vs_raw"] = (df["baris_dipindai"] / df["baris_dipindai"].iloc[0]).round(3)
    print(df.to_string(index=False))
    print()

    n_sensor = 100
    qpm = 10
    menit = 60
    total_query = n_sensor * qpm * menit
    print(f"biaya kumulatif: {n_sensor} sensor x {qpm} query/menit x {menit} menit = {total_query} query")
    baris2 = []
    for label, info in skenario:
        bpq = info["baris_dipindai"]
        total_baris = bpq * total_query
        baris2.append({
            "skenario": label,
            "baris_per_query": bpq,
            "total_baris_per_jam": total_baris,
        })
    df2 = pd.DataFrame(baris2)
    print(df2.to_string(index=False))
    print()

    baris_per_detik = 1_000_000
    print(f"asumsi throughput pemindaian kasar: {baris_per_detik} baris/detik")
    for label, info in skenario:
        bpq = info["baris_dipindai"]
        detik = (bpq * total_query) / baris_per_detik
        print(f"  {label}: {detik:.2f} detik CPU/jam")
    print()

    print("biaya refresh cagg harian (sekali per hari):")
    refresh_baris = HARI_MENTAH * TITIK_PER_HARI
    detik = refresh_baris / baris_per_detik
    print(f"  pindai {refresh_baris} baris = {detik:.2f} detik (sekali/hari)")
    print()

    print("padanan SQL TimescaleDB (prakomputasi continuous aggregate):")
    print("  CREATE MATERIALIZED VIEW suhu_harian")
    print("  WITH (timescaledb.continuous = true) AS")
    print("  SELECT time_bucket('1 day', ts) AS bucket, sensor_id,")
    print("         avg(suhu_c) FROM suhu_sensor")
    print("  GROUP BY bucket, sensor_id WITH NO DATA;")
    print()
    print("padanan SQL TimescaleDB (ad-hoc):")
    print("  SELECT time_bucket('1 day', ts) AS bucket, avg(suhu_c)")
    print("  FROM suhu_sensor")
    print("  WHERE ts >= now() - INTERVAL '7 days'")
    print("  GROUP BY bucket ORDER BY bucket;")


if __name__ == "__main__":
    main()
text Keluaran
skenario: 1 sensor, 1 titik/menit, 30 hari data mentah
total baris mentah: 43200

query dashboard: rerata harian 7 hari terakhir
    skenario            sumber  baris_dipindai  rasio_vs_raw
ad-hoc (raw) hypertable mentah           10080         1.000
cagg per jam      cagg per jam             168         0.017
 cagg harian       cagg harian               7         0.001

biaya kumulatif: 100 sensor x 10 query/menit x 60 menit = 60000 query
    skenario  baris_per_query  total_baris_per_jam
ad-hoc (raw)            10080            604800000
cagg per jam              168             10080000
 cagg harian                7               420000

asumsi throughput pemindaian kasar: 1000000 baris/detik
  ad-hoc (raw): 604.80 detik CPU/jam
  cagg per jam: 10.08 detik CPU/jam
  cagg harian: 0.42 detik CPU/jam

biaya refresh cagg harian (sekali per hari):
  pindai 43200 baris = 0.04 detik (sekali/hari)

padanan SQL TimescaleDB (prakomputasi continuous aggregate):
  CREATE MATERIALIZED VIEW suhu_harian
  WITH (timescaledb.continuous = true) AS
  SELECT time_bucket('1 day', ts) AS bucket, sensor_id,
         avg(suhu_c) FROM suhu_sensor
  GROUP BY bucket, sensor_id WITH NO DATA;

padanan SQL TimescaleDB (ad-hoc):
  SELECT time_bucket('1 day', ts) AS bucket, avg(suhu_c)
  FROM suhu_sensor
  WHERE ts >= now() - INTERVAL '7 days'
  GROUP BY bucket ORDER BY bucket;

Keluaran di atas memperlihatkan tiga hal. Pertama, rasio baris yang dipindai turun dari 1.000 pada ad-hoc menjadi 0.017 pada continuous aggregate per jam dan 0.001 pada continuous aggregate harian, sehingga query ke continuous aggregate harian hanya memindai 1/1000 dari query ad-hoc. Kedua, biaya kumulatif per jam pada 100 sensor yang masing-masing menjalankan 10 query per menit mencapai 604.800.000 baris pada ad-hoc, sedangkan pada continuous aggregate harian hanya 420.000 baris. Ketiga, dengan asumsi throughput pemindaian kasar satu juta baris per detik, ad-hoc menghabiskan sekitar 605 detik CPU per jam, sedangkan continuous aggregate harian hanya 0,42 detik CPU per jam, atau sekitar 1.400 kali lebih hemat.

Biaya kumulatif pada beban dashboard#

Biaya per query saja tidak menjelaskan mengapa prakomputasi menjadi keputusan desain, sebab satu query ad-hoc yang memindai 10.080 baris masih cepat. Yang mengubah kalkulasi adalah biaya kumulatif pada beban dashboard yang sibuk. Pada 100 sensor yang masing-masing menjalankan 10 query per menit selama satu jam, total query mencapai 60.000, sehingga total baris yang dipindai pada ad-hoc mencapai 604,8 juta baris per jam. Angka ini bukan sekadar besar, tetapi berulang setiap jam, sehingga basis data bekerja tanpa henti untuk query yang sebenarnya menjawab pertanyaan yang sama.

Prakomputasi memecahkan masalah ini dengan memindahkan pekerjaan agregasi dari sisi query ke sisi refresh yang jauh lebih jarang. Continuous aggregate harian disegarkan sekali per hari, sedangkan query yang datang setelahnya hanya membaca tujuh baris per query. Pada contoh di atas, biaya refresh harian hanya 0,04 detik CPU untuk memindai 43.200 baris sekali, sehingga total beban CPU per jam turun dari 605 detik menjadi 0,42 detik. Penghematan ini bukan sekadar optimasi, melainkan perbedaan antara basis data yang stabil dan basis data yang tercekik oleh dashboard.

Biaya refresh: memindahkan beban ke latar belakang#

Biaya refresh continuous aggregate adalah pemindaian rentang tertentu pada hypertable mentah, diikuti penulisan ulang baris teragregasi pada tabel materialisasi. Pada continuous aggregate harian dengan 30 hari data, refresh harian memindai 43.200 baris, yang dengan asumsi throughput kasar satu juta baris per detik hanya memakan 0,04 detik. Biaya ini dibayar sekali per hari, sehingga total beban refresh per jam hanya sekitar 0,0017 detik CPU, jauh lebih kecil daripada 605 detik CPU per jam pada ad-hoc. Tradeoff utama bukan tanpa biaya, sebab continuous aggregate memerlukan ruang penyimpanan tambahan, tetapi sebagaimana dijelaskan pada topik sebelumnya, ukuran rollup jauh lebih kecil daripada data mentah.

Pemilihan jadwal refresh bergantung pada seberapa segar data yang diharapkan oleh dashboard. Jadwal refresh setiap satu jam dengan end_offset satu jam memberikan keterlambatan maksimum dua jam, sedangkan jadwal refresh setiap lima belas menit dengan end_offset lima belas menit memberikan keterlambatan maksimum tiga puluh menit. Bila dashboard menuntut data hingga menit terakhir, mode real-time aggregates dengan materialized_only = false menggabungkan baris termaterialisasi dengan baris segar pada hypertable sumber, sehingga keterlambatan turun ke nol dengan biaya query yang sedikit lebih tinggi. Pilihan ini dijelaskan lebih lanjut pada topik continuous aggregates.

Diagram alur prakomputasi vs ad-hoc#

Diagram berikut membandingkan dua jalur pemindaian untuk beban dashboard yang sama. Pada jalur ad-hoc, 60.000 query per jam masing-masing memindai 10.080 baris pada hypertable mentah, sehingga total mencapai 604.800 baris per jam atau sekitar 605 detik CPU per jam. Pada jalur prakomputasi, pekerjaan agregasi dipindahkan ke refresh job yang berjalan sekali per hari, sehingga 60.000 query per jam masing-masing hanya memindai 7 baris pada continuous aggregate harian, dengan total 42.000 baris per jam atau sekitar 0,42 detik CPU per jam.

Beban ad-hocBeban prakomputasiraw hypertable10.080 baris dipindai per queryraw hypertable (43200 baris)refresh 1x/haricagg harian7 baris dipindai per query100 sensor x 10 qpm = 60000 query/jam604.800 baris dipindai/jam~605 detik CPU/jam42.000 baris dipindai/jam~0,42 detik CPU/jam
Perbandingan beban ad-hoc vs prakomputasi: 60000 query/jam memindai 604.800 baris pada ad-hoc, sedangkan pada prakomputasi hanya 42.000 baris

Kesalahan umum#

  • Menghitung rollup secara ad-hoc pada setiap query. Pendekatan ini menyebabkan setiap dashboard memproses ulang agregasi yang sama, sehingga keuntungan prakomputasi hilang.
  • Mengharapkan continuous aggregate selalu lebih cepat tanpa memeriksa kebijakan refresh. Bila kebijakan refresh tidak dijalankan atau rentang refresh tidak mencakup rentang query, hasilnya dapat mengandung gap atau data basi.
  • Membandingkan biaya ad-hoc dengan biaya refresh tanpa memperhitungkan keduanya. Prakomputasi memindahkan beban, bukan menghilangkannya, sehingga perhitungan total harus memasukkan biaya refresh dan biaya query dari continuous aggregate.

Ringkasan#

  • Prakomputasi memindahkan pemindaian dari sisi query ke sisi refresh yang jauh lebih jarang, sehingga beban kumulatif dashboard turun drastis.
  • Pada skenario 100 sensor × 10 query per menit × 60 menit, ad-hoc memindai 604,8 juta baris per jam, sedangkan continuous aggregate harian hanya 420 ribu baris per jam, atau sekitar 1.400 kali lebih hemat.
  • Biaya refresh continuous aggregate harian hanya 0,04 detik CPU sekali per hari, sehingga total beban CPU per jam turun dari 605 detik menjadi 0,42 detik.

Referensi#