Kardinalitas adalah banyaknya nilai unik pada sebuah kolom. Topik ini menjelaskan konsep anggaran kardinalitas, yaitu memprediksi jumlah series yang dihasilkan oleh skema sebelum basis data dijalankan, serta menjelaskan mengapa ID unik yang dijadikan tag menjadi sumber utama basis data time-series yang menenggelamkan diri sendiri. Setelah membaca, setiap kolom yang dijadikan tag memiliki angka perkiraan jumlah series, bukan dugaan.
Apa itu kardinalitas dan kenapa dihitung ?#
Kardinalitas tag pada basis data time-series menentukan ukuran indeks series, yaitu struktur yang memetakan setiap kombinasi nilai tag ke lokasi data. InfluxDB sejak era 1.x dan 2.x mengenal istilah series cardinality, yaitu hasil kali kardinalitas seluruh tag pada sebuah measurement. TimescaleDB tidak memiliki konsep series yang eksplisit, tetapi indeks pada kolom identitas tumbuh sebanding banyaknya nilai unik, sehingga konsekuensinya serupa. Karena itu, anggaran kardinalitas berlaku umum pada basis data time-series apa pun, meskipun istilah teknisnya berbeda.
Penghitungan kardinalitas wajib dilakukan sebelum skema ditetapkan, sebab biaya indeks series tidak proporsional terhadap jumlah baris, melainkan terhadap hasil kali kardinalitas tag. Sebuah skema dengan dua tag yang masing-masing memiliki 100 nilai unik menghasilkan 10.000 series, sedangkan skema dengan tiga tag yang masing-masing 100 nilai unik menghasilkan 1.000.000 series. Pertumbuhan eksponensial ini berarti satu tag berdimensi tinggi cukup untuk membuat indeks meledak, terlepas dari apakah tag lainnya rendah kardinalitas.
Ledakan series: perkalian kardinalitas tag#
Rumus jumlah series adalah hasil kali kardinalitas seluruh tag pada sebuah measurement. Bila tag adalah station_id dengan 50 nilai dan lokasi dengan 3 nilai, jumlah series adalah 150. Bila ditambahkan tag user_id dengan 100.000 nilai, jumlah series melonjak menjadi 5 juta. Pertumbuhan ini bukan tambahan, melainkan perkalian, sehingga setiap tag berdimensi tinggi menjadi pengganda, bukan penambah.
Basis data time-series yang menyimpan indeks series di memori akan kesulitan ketika jumlah series mencapai jutaan. InfluxDB 1.x dan 2.x memiliki batas praktis sekitar 10 juta series per measurement, melampaui itu performa penulisan dan query menurun tajam. InfluxDB 3.x dirancang ulang untuk mengatasi batasan ini, tetapi biaya indeks tetap ada, hanya berpindah dari memori ke penyimpanan kolumnar. TimescaleDB tidak memiliki batas eksplisit, tetapi indeks B-tree pada kolom dengan jutaan nilai unik memperlambat penulisan dan memperbesar ukuran indeks.
Anggaran kardinalitas menuntut keputusan tentang kolom mana yang layak menjadi tag. Aturan praktisnya adalah memilih kolom yang kardinalitasnya jauh lebih rendah dari jumlah baris yang diharapkan, dan yang sering muncul sebagai predikat WHERE. Kolom yang nilainya unik per titik, misalnya ID transaksi atau ID sesi, tidak memenuhi kriteria ini, sebab pertambahannya sebanding dengan jumlah baris. Kolom yang nilainya berulang dalam rentang waktu lama, misalnya region atau service, justru menjadi tag yang baik, sebab pertambahannya lambat dan pola query sering menyaring berdasarkan keduanya.
Menghitung anggaran kardinalitas dengan pandas#
Skrip berikut menghitung jumlah series untuk tiga skenario skema yang berbeda, yaitu tag stasiun saja, tag stasiun dan lokasi, serta tag stasiun dan ID pengguna. Skrip juga menghitung perkiraan ukuran indeks series dengan asumsi konservatif 100 byte per entri. Tujuannya menunjukkan secara konkret bahwa satu tag berdimensi tinggi cukup untuk membuat indeks membengkak dari kilobita menjadi ratusan megabita.
"""Anggaran kardinalitas pada data time-series.
Skrip menghitung jumlah *series* (kombinasi unik dari nilai tag) untuk
tiga skenario skema yang berbeda, yaitu tag stasiun saja, tag stasiun
dan lokasi, serta tag stasiun dan ID pengguna. Tujuannya menunjukkan
bahwa menjadikan ID unik sebagai tag membuat jumlah *series* tumbuh
sebanding jumlah baris, sehingga indeks membengkak dan basis data
menenggelamkan diri sendiri. Skrip juga menghitung perkiraan ukuran
indeks series sebagai ilustrasi biaya.
"""
from __future__ import annotations
import pandas as pd
TITIK_PER_SERIES = 1_000 # asumsi: 1000 titik per series
BYTE_PER_SERIES = 100 # asumsi konservatif: 100 byte per entri indeks series
def hitung_series(tags_cardinalitas: dict[str, int]) -> int:
"""Jumlah series = hasil kali kardinalitas seluruh tag."""
hasil = 1
for c in tags_cardinalitas.values():
hasil *= c
return hasil
def perkiraan_index_kb(jumlah_series: int) -> float:
return jumlah_series * BYTE_PER_SERIES / 1024.0
def main() -> None:
skenario = [
("tag station_id saja", {"station_id": 50}),
("tag station_id dan lokasi", {"station_id": 50, "lokasi": 3}),
("tag station_id dan user_id (high cardinality)", {"station_id": 50, "user_id": 100_000}),
]
print(f"asumsi: {TITIK_PER_SERIES} titik per series, {BYTE_PER_SERIES} byte per entri indeks series")
print()
rows = []
for nama, tags in skenario:
series = hitung_series(tags)
kb = perkiraan_index_kb(series)
baris = series * TITIK_PER_SERIES
rows.append({
"skenario": nama,
"kombinasi_tag": " x ".join(f"{k}={v}" for k, v in tags.items()),
"jumlah_series": series,
"total_titik": baris,
"indeks_series_kb": round(kb, 1),
})
df = pd.DataFrame(rows)
print(df.to_string(index=False))
print()
# kontras: user_id diturunkan menjadi field
print("bila user_id diturunkan menjadi field:")
series_baru = 50 * 3
print(f" kombinasi tag tersisa: station_id=50 x lokasi=3 = {series_baru} series")
print(f" indeks series: {perkiraan_index_kb(series_baru):.1f} KB")
print(f" catatan: total titik tetap tinggi, tetapi tidak memengaruhi indeks series")
print()
s_tanpa = 50 * 3
s_dengan = 50 * 100_000
print(f"rasio jumlah series: {s_dengan / s_tanpa:.0f}x lipat saat user_id menjadi tag")
if __name__ == "__main__":
main()asumsi: 1000 titik per series, 100 byte per entri indeks series
skenario kombinasi_tag jumlah_series total_titik indeks_series_kb
tag station_id saja station_id=50 50 50000 4.9
tag station_id dan lokasi station_id=50 x lokasi=3 150 150000 14.6
tag station_id dan user_id (high cardinality) station_id=50 x user_id=100000 5000000 5000000000 488281.2
bila user_id diturunkan menjadi field:
kombinasi tag tersisa: station_id=50 x lokasi=3 = 150 series
indeks series: 14.6 KB
catatan: total titik tetap tinggi, tetapi tidak memengaruhi indeks series
rasio jumlah series: 33333x lipat saat user_id menjadi tagKeluaran di atas memperlihatkan tiga hal. Pertama, skenario pertama menghasilkan 50 series dengan indeks sekitar 4,9 KB. Kedua, skenario kedua menghasilkan 150 series dengan indeks sekitar 14,6 KB, yaitu pertumbuhan tiga kali lipat sebanding dengan tambahan kardinalitas lokasi. Ketiga, skenario ketiga menghasilkan 5 juta series dengan indeks sekitar 488 MB, yaitu pertumbuhan 33.333 kali lipat dari skenario kedua. Saat user_id diturunkan menjadi field, jumlah series kembali ke 150 dan indeks kembali ke 14,6 KB, meskipun total titik tetap tinggi. Inilah inti anggaran kardinalitas, yaitu memilih tag berdasarkan kardinalitas, bukan berdasarkan keinginan untuk menyaring.
Kesalahan umum#
- Menjadikan ID transaksi sebagai tag dengan alasan “kelak ingin menyaring berdasarkan ID”. Penyaringan berdasarkan ID unik adalah query berbasis nilai, bukan pola time-series, sehingga sebaiknya dilakukan pada basis data operasional, bukan basis data time-series.
- Menganggap anggaran kardinalitas hanya relevan untuk InfluxDB 1.x dan 2.x. InfluxDB 3.x dan TimescaleDB tetap membayar biaya indeks, hanya dalam bentuk berbeda, sehingga perhitungan tetap perlu.
- Menggabungkan beberapa dimensi menjadi satu tag, misalnya
station_id_lokasi=STA01-atap. Kolom gabungan mengaburkan kardinalitas dan menyulitkan perhitungan, sebab series-nya tetap hasil kali, tetapi tidak bisa dianalisis per dimensi.
Ringkasan#
- Anggaran kardinalitas adalah prediksi jumlah series sebelum basis data dijalankan, dihitung sebagai hasil kali kardinalitas seluruh tag.
- Satu tag berdimensi tinggi cukup untuk membuat indeks series meledak, terlepas dari kardinalitas tag lainnya.
- ID unik sebaiknya disimpan sebagai field, bukan tag, sebab query berdasarkan ID unik bukan pola time-series.
Referensi#
- Resolve high cardinality — InfluxDB — Panduan resmi InfluxDB untuk mengidentifikasi dan menurunkan kardinalitas series yang membengkak.
- Design your data model — Timescale — Panduan resmi TimescaleDB untuk merancang model data hypertable beserta pemilihan kolom yang diindeks.
- Designing Data-Intensive Applications — O’Reilly — Buku referensi yang membahas indeks B-tree dan LSM-tree beserta implikasinya pada beban tulis berdimensi tinggi.