InfluxDB 3.x mengganti dua pilar inti dari pendahulunya, yaitu mesin penyimpanan dan mesin query, sehingga postur basis data ini berbeda dari 1.x dan 2.x pada level fundamental. Topik ini membahas bentuk penyimpanan kolumnar berbasis Apache Arrow dan Parquet, lalu menjelaskan posisi SQL sebagai bahasa utama di atas Apache DataFusion pada tingkat kesadaran, tanpa mendalami sintaks detail. Setelah membaca topik ini, alasan InfluxDB 3.x disebut sebagai basis data time-series kolumnar yang berbicara SQL menjadi jelas, sekaligus menjelaskan mengapa banyak praktik lama dari era TSM tidak lagi berlaku.
Penyimpanan kolumnar berbasis Arrow#
Apache Arrow adalah format in-memory kolumnar yang menjadi memori utama InfluxDB 3.x saat menjalankan query. Data disusun per kolom, bukan per baris, sehingga seluruh nilai suhu_c berdekatan dalam satu buffer memori, demikian pula seluruh nilai ts dan sensor_id. Tata letak ini memberi dua keuntungan langsung. Pertama, query yang hanya menyentuh beberapa kolom, misalnya SELECT suhu_c FROM ..., hanya membaca buffer kolom suhu_c dan membiarkan kolom lain di disk maupun di memori. Kedua, kolom dengan nilai berdekat atau berulang menjadi kandidat kompresi yang sangat baik, sebab dictionary encoding, run-length, dan delta encoding bekerja optimal pada deret nilai yang homogen.
Apache Parquet menjadi format on-disk. Parquet adalah format kolumnar berseri yang sudah lama digunakan di ekosistem data, sehingga berkas Parquet yang ditulis oleh InfluxDB 3.x dapat dibaca langsung oleh DuckDB, pandas, Spark, dan banyak alat lain. Sifat interoperabel ini menjadi perbedaan besar dibanding era TSM, sebab berkas .tsm hanya bisa dibaca oleh InfluxDB sendiri. Karena itu, ekosistem alat analitik tidak lagi menganggap InfluxDB sebagai pulau, melainkan sebagai salah satu produsen berkas Parquet dalam pipeline yang lebih luas.
SQL sebagai bahasa utama#
InfluxDB 3.x menggunakan Apache DataFusion sebagai mesin query, sehingga SQL menjadi bahasa utama. Sebuah tabel telemetri pada 3.x memiliki kolom time sebagai cap waktu, kolom tag seperti sensor_id, dan kolom field seperti suhu_c atau kelembapan, semuanya sebagai kolom biasa yang dapat digunakan dalam klausa SELECT, WHERE, GROUP BY, atau JOIN tanpa sintaks khusus. InfluxQL tetap didukung sebagai opsi kompatibilitas untuk pengguna 1.x yang bermigrasi, sedangkan Flux dinyatakan tidak akan dikembangkan lebih lanjut, sehingga SQL adalah pilihan yang tahan lama.
Pilihan SQL memberi tiga konsekuensi praktis. Pertama, keahlian pengguna menjadi portabel, sebab SQL yang ditulis untuk InfluxDB 3.x sebagian besar dapat ditulis ulang untuk Postgres atau DuckDB dengan perubahan kecil. Kedua, integrasi dengan alat eksternal menjadi mudah, sebab banyak alat business intelligence dan notebook sudah mahir SQL. Ketiga, kompleksitas transformasi pipeline yang dahulu ditulis dalam Flux kini dapat dinyatakan dalam SELECT ... FROM ... JOIN ..., sehingga kurva belajar yang curam pada era 2.x tidak lagi ada. Pernyataan SQL berikut tidak dijalankan di sandbox, sebab tidak ada influx atau DataFusion yang terpasang, sehingga hanya menjadi referensi sintaksis.
-- rerata suhu per sensor selama 1 jam terakhir
SELECT
sensor_id,
date_trunc('hour', time) AS jam,
AVG(suhu_c) AS suhu_rerata
FROM suhu
WHERE time >= NOW() - INTERVAL '1 hour'
GROUP BY sensor_id, jam
ORDER BY sensor_id, jam;sensor_id|jam|suhu_rerata
---|---|---
S01|2025-01-01T00:00:00|24.389166666666664
S02|2025-01-01T00:00:00|25.3Column pruning dalam praktik#
Column pruning adalah keuntungan paling nyata dari penyimpanan kolumnar pada beban time-series. Bila query hanya menyentuh tiga dari sepuluh kolom, mesin hanya membaca tiga kolom itu dari disk dan membiarkan sisanya. Pada row store seperti TSM, pembacaan satu kolom tetap membaca seluruh baris dari disk, sebab data disimpan per baris, sehingga disk I/O menjadi jauh lebih besar daripada yang dibutuhkan query. Demo berikut memperlihatkan perbedaan ini secara visual pada tabel kecil.
Kode sumber demo
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<title>Row store vs kolumnar</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; margin: 16px; color: #1f2937; }
h2 { font-size: 16px; margin: 0 0 8px 0; font-weight: 600; }
p.ket { font-size: 13px; color: #4b5563; margin: 0 0 12px 0; }
.tombol { display: flex; gap: 8px; margin-bottom: 12px; }
button {
font: inherit; font-size: 13px; padding: 6px 12px; border-radius: 4px;
border: 1px solid #d1d5db; background: #f9fafb; cursor: pointer;
}
button.aktif { background: #1e3a8a; color: #fff; border-color: #1e3a8a; }
table { border-collapse: collapse; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
th, td { border: 1px solid #e5e7eb; padding: 6px 10px; text-align: left; transition: background 0.15s; }
th { background: #f3f4f6; font-weight: 600; }
td.dibaca { background: #fde68a; }
td.dibaca-kolom { background: #bfdbfe; }
.status { margin-top: 12px; font-size: 13px; padding: 8px 12px; border-radius: 4px; background: #f1f5f9; }
.status .bilangan { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; }
</style>
</head>
<body>
<h2>Telemetri sensor suhu (5 baris, 4 kolom)</h2>
<p class="ket">Klik tombol untuk melihat sel mana yang dibaca saat query <code>SELECT suhu_c</code> dijalankan pada masing-masing mesin.</p>
<div class="tombol">
<button id="btn-row" type="button">Pindai row store</button>
<button id="btn-kol" type="button">Pindai kolumnar (suhu_c)</button>
<button id="btn-reset" type="button">Reset</button>
</div>
<table id="tabel">
<thead>
<tr>
<th>time</th><th>sensor_id</th><th>suhu_c</th><th>kelembapan</th>
</tr>
</thead>
<tbody>
<tr><td>00:00</td><td>S01</td><td>22.1</td><td>60.3</td></tr>
<tr><td>00:01</td><td>S01</td><td>22.2</td><td>61.4</td></tr>
<tr><td>00:02</td><td>S02</td><td>22.5</td><td>59.6</td></tr>
<tr><td>00:03</td><td>S02</td><td>22.6</td><td>58.9</td></tr>
<tr><td>00:04</td><td>S03</td><td>22.4</td><td>60.1</td></tr>
</tbody>
</table>
<div class="status" id="status">Belum ada pemindaian. Klik salah satu tombol di atas.</div>
<script>
const tabel = document.getElementById("tabel");
const status = document.getElementById("status");
const sel = () => Array.from(tabel.querySelectorAll("tbody td"));
function bersihkan() {
sel().forEach(td => { td.classList.remove("dibaca", "dibaca-kolom"); });
document.querySelectorAll(".tombol button").forEach(b => b.classList.remove("aktif"));
}
function pindaiRow() {
bersihkan();
document.getElementById("btn-row").classList.add("aktif");
// Row store membaca seluruh baris walaupun hanya suhu_c yang dipakai.
sel().forEach(td => td.classList.add("dibaca"));
const kolom = 4, baris = 5;
status.innerHTML =
"Mesin membaca seluruh baris (row store): " +
"<span class='bilangan'>" + (kolom * baris) + " sel</span> dari disk, " +
"tetapi query hanya memakai kolom suhu_c.";
}
function pindaiKolom() {
bersihkan();
document.getElementById("btn-kol").classList.add("aktif");
// Kolumnar hanya membaca kolom yang dipakai query (suhu_c = kolom ke-3).
const baris = 5;
tabel.querySelectorAll("tbody tr").forEach(tr => {
const tdSuhu = tr.children[2];
tdSuhu.classList.add("dibaca-kolom");
});
status.innerHTML =
"Mesin membaca hanya kolom suhu_c (kolumnar): " +
"<span class='bilangan'>" + baris + " sel</span> dari disk, " +
"kolom lain tidak disentuh sama sekali.";
}
function reset() {
bersihkan();
status.textContent = "Belum ada pemindaian. Klik salah satu tombol di atas.";
}
document.getElementById("btn-row").addEventListener("click", pindaiRow);
document.getElementById("btn-kol").addEventListener("click", pindaiKolom);
document.getElementById("btn-reset").addEventListener("click", reset);
</script>
</body>
</html>Demo di atas menampilkan tabel telemetri sederhana dengan enam kolom. Tombol Pindai row store menyala pada seluruh sel, sebab seluruh baris harus dibaca dari disk walaupun hanya satu kolom yang digunakan. Tombol Pindai kolumnar hanya menyala pada kolom suhu_c, sebab mesin kolumnar hanya membaca kolom yang digunakan query. Pada tabel kecil ini, perbedaannya tampak sepele, tetapi pada tabel dengan puluhan kolom dan miliaran baris, perbedaan disk I/O menjadi beberapa kali lipat, sehingga query analitik yang lazim di beban time-series berjalan jauh lebih cepat.
Mock struktur Arrow#
Karena Apache Arrow tidak terpasang sebagai pustaka yang siap digunakan di sandbox ini, skrip berikut memodelkan struktur kolom Arrow secara sederhana dengan list Python. Tujuannya bukan mereproduksi representasi memori sebenarnya, melainkan menunjukkan bahwa tiap kolom adalah unit terpisah yang dapat diakses tanpa menyentuh kolom lain. Skrip tidak menjalankan SQL dan tidak memerlukan pustaka Arrow.
"""Mock sederhana struktur kolom Apache Arrow.
Skrip ini tidak menjalankan SQL dan tidak memerlukan pustaka Arrow. Tujuannya
memperlihatkan bahwa pada penyimpanan kolumnar, tiap kolom adalah unit terpisah
yang dapat diakses tanpa menyentuh kolom lain. Angka byte per kolom adalah
perkiraan kasar untuk ilustrasi column pruning.
"""
# Empat kolom dari tabel telemetri suhu, disusun per kolom (bukan per baris).
KOLOM = {
"time": ["00:00", "00:01", "00:02", "00:03", "00:04"],
"sensor_id": ["S01", "S01", "S02", "S02", "S03"],
"suhu_c": [22.1, 22.2, 22.5, 22.6, 22.4],
"kelembapan": [60.3, 61.4, 59.6, 58.9, 60.1],
}
# Perkiraan byte per elemen (hanya untuk ilustrasi column pruning).
BYTE_PER_ELEMEN = {
"time": 8, # cap waktu sebagai int64
"sensor_id": 6, # string pendek rata-rata
"suhu_c": 8, # double
"kelembapan": 8, # double
}
def ukuran_kolom(nama: str) -> int:
return len(KOLOM[nama]) * BYTE_PER_ELEMEN[nama]
def pindai_row_store(kolom_dipakai: list[str]) -> int:
# Row store membaca seluruh baris walaupun hanya satu kolom yang dipakai.
return sum(ukuran_kolom(k) for k in KOLOM)
def pindai_kolumnar(kolom_dipakai: list[str]) -> int:
# Kolumnar hanya membaca kolom yang dipakai query.
return sum(ukuran_kolom(k) for k in kolom_dipakai)
def main() -> None:
total_row = pindai_row_store([])
print(" tabel suhu (5 baris, 4 kolom):")
for k, v in KOLOM.items():
print(f" kolom {k:<10} {v}")
print()
print(f"ukuran total seluruh kolom (row store): {total_row} byte")
print()
kasus = [
("SELECT suhu_c", ["suhu_c"]),
("SELECT suhu_c, kelembapan", ["suhu_c", "kelembapan"]),
("SELECT sensor_id, suhu_c", ["sensor_id", "suhu_c"]),
]
for judul, kolom in kasus:
row = pindai_row_store(kolom)
col = pindai_kolumnar(kolom)
rasio = (1 - col / row) * 100
print(f"{judul:<32} row={row:>3} byte kolumnar={col:>3} byte (hemat {rasio:.0f}%)")
if __name__ == "__main__":
main() tabel suhu (5 baris, 4 kolom):
kolom time ['00:00', '00:01', '00:02', '00:03', '00:04']
kolom sensor_id ['S01', 'S01', 'S02', 'S02', 'S03']
kolom suhu_c [22.1, 22.2, 22.5, 22.6, 22.4]
kolom kelembapan [60.3, 61.4, 59.6, 58.9, 60.1]
ukuran total seluruh kolom (row store): 150 byte
SELECT suhu_c row=150 byte kolumnar= 40 byte (hemat 73%)
SELECT suhu_c, kelembapan row=150 byte kolumnar= 80 byte (hemat 47%)
SELECT sensor_id, suhu_c row=150 byte kolumnar= 70 byte (hemat 53%)Keluaran di atas memperlihatkan bahwa column pruning memberi penghematan yang signifikan pada query yang menyentuh sedikit kolom. Query SELECT suhu_c hanya membaca 40 byte dari 150 byte total, sehingga disk I/O turun 73 persen. Pada telemetri nyata dengan puluhan kolom, penghematan ini bisa mencapai 90 persen lebih, sehingga query analitik yang lazim pada basis data time-series, misalnya rerata suhu per jam, berjalan jauh lebih cepat pada mesin kolumnar daripada pada row store.
Kesalahan umum#
- Menganggap Apache Arrow dan Parquet adalah dua hal yang saling menggantikan. Arrow adalah format in-memory, sedangkan Parquet adalah format on-disk, dan keduanya saling melengkapi di dalam InfluxDB 3.x.
- Membawa asumsi series cardinality dari era 1.x ke 3.x dengan tetap membatasi jumlah tag unik secara ketat. Pada 3.x, ledakan seri tidak terjadi sebab mesin kolumnar tidak mengindeks per seri, sehingga batas praktis berbeda jauh.
- Mengira SQL pada InfluxDB 3.x identik dengan SQL Postgres. DataFusion mendukung sebagian besar standar SQL, tetapi ada fungsi dan tipe data yang tidak hadir, sehingga sintaks tertentu, misalnya window function tertentu atau ekstensi Postgres seperti
jsonb, perlu penyesuaian.
Ringkasan#
- InfluxDB 3.x menyimpan data dalam format kolumnar Apache Arrow di memori dan Apache Parquet di disk, sehingga query yang menyentuh sedikit kolom membaca jauh lebih sedikit byte daripada row store.
- SQL di atas Apache DataFusion menjadi bahasa query utama, sedangkan InfluxQL tetap didukung sebagai opsi kompatibilitas dan Flux ditinggalkan, sehingga keahlian pengguna menjadi portabel ke ekosistem data yang lebih luas.
- Berkas Parquet yang dihasilkan dapat dibaca oleh DuckDB, pandas, dan Spark, sehingga InfluxDB 3.x tidak lagi menjadi pulau dalam pipeline data.
Referensi#
- InfluxDB 3 Core storage — InfluxData — Penjelasan resmi konsep penyimpanan 3.x, termasuk peran Apache Arrow dan Parquet.
- Apache Arrow format specification — The Apache Software Foundation — Spesifikasi format in-memory kolumnar yang menjadi dasar InfluxDB 3.x.
- Apache DataFusion — The Apache Software Foundation — Mesin query SQL yang digunakan InfluxDB 3.x, dengan dokumentasi fungsi dan tipe data yang didukung.