Tim operasional TokoKita ingin dashboard yang berkedip setiap ada pesanan baru: berapa order per menit, dari kota mana, produk apa yang sedang naik. Dashboard semacam itu dibaca ratusan kali per detik dan berjalan di atas miliaran baris event yang terus bertambah — bukan pekerjaan batch sekali sehari. ClickHouse adalah database OLAP columnar yang dibangun khusus untuk profil ini: penambahan data (insert) sangat cepat, agregasi atas miliaran baris selesai dalam milidetik, dan ribuan query bersamaan dilayani tanpa saling mengganggu. Seperti chapter Trino, ClickHouse tidak terpasang di lingkungan penulisan — semua contoh SQL ditandai “berjalan di ClickHouse” dan konfigurasi divalidasi strukturnya lewat Python.
MergeTree: Tabel Terurut dengan Sparse Primary Index #
Mesin tabel paling penting di ClickHouse bernama MergeTree. Data disisipkan sebagai part baru, lalu part-part itu digabung (merge) di latar belakang. Kunci pentingnya: baris disimpan terurut menurut ORDER BY, dan ClickHouse hanya menyimpan penanda posisi tiap rentang baris (sparse primary index) — bukan indeks per baris seperti B-tree di OLTP:
-- Berjalan di ClickHouse
CREATE TABLE tokokita.event_pesanan
(
waktu DateTime,
pesanan_id UInt64,
kota LowCardinality(String),
produk_id UInt32,
nilai Float64
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(waktu)
ORDER BY (kota, waktu)
SETTINGS index_granularity = 8192;Empat keputusan desain terlihat di DDL tersebut. PARTITION BY toYYYYMM(waktu) memisahkan data per bulan sehingga query rentang bulanan hanya membuka partisi relevan. ORDER BY (kota, waktu) menentukan urutan fisik penyimpanan — query WHERE kota = 'Jakarta' AND waktu >= now() - INTERVAL 1 HOUR melompat langsung ke rentang baris Jakarta pada jam terakhir. Tipe LowCardinality(String) memanfaatkan kamus internal untuk kolom berulang seperti nama kota. Terakhir, index_granularity = 8192 berarti satu penanda indeks dibuat tiap 8.192 baris — itulah makna sparse.
Query dashboard-nya lalu sesederhana ini:
-- Berjalan di ClickHouse
SELECT toStartOfMinute(waktu) AS menit,
count() AS jumlah,
round(sum(nilai), 0) AS total
FROM tokokita.event_pesanan
WHERE waktu >= now() - INTERVAL 15 MINUTE
GROUP BY menit
ORDER BY menit;Untuk angka yang dihitung berulang, ClickHouse menawarkan materialized view: tabel ringkasan yang diperbarui otomatis saat data baru masuk, biasanya dengan engine SummingMergeTree — pola agregasi-awal yang membuat dashboard tinggal membaca ribuan baris ringkasan alih-alih miliaran baris mentah.
Arsitektur: Shard dan Replica #
Skala horizontal ClickHouse memakai dua sumbu sekaligus. Shard memecah data ke banyak mesin; replica menduplikasi shard yang sama demi ketersediaan dan pemecahan beban baca:
Replikasi bersifat asynchronous: INSERT dianggap selesai begitu satu replica menuliskannya, lalu baris menyebar ke replica lain lewat replication log — koordinasinya ditangani ClickHouse Keeper. Konsekuensinya jujur saja: replica bisa sempat tertinggal beberapa detik, dan query yang menempati replica tertinggal bisa melihat data yang sedikit lebih lama.
Konfigurasi Server #
Konfigurasi ClickHouse ditulis dalam XML di bawah config.d (nilai kecil menimpa bawaan) dan server paling ringkas dihidupkan lewat container. Kedua berkas berikut divalidasi strukturnya secara nyata dengan yaml.safe_load dan parser XML Python — bukan dijalankan:
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
ulimits:
nofile: 262144
ports:
- "8123:8123"
- "9000:9000"
volumes:
- ./config.d:/etc/clickhouse-server/config.d<clickhouse>
<max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
<mark_cache_size>536870912</mark_cache_size>
<timezone>Asia/Jakarta</timezone>
</clickhouse>mark_cache_size mengalokasikan memori untuk cache penanda granule — kumpulan alamat rentang baris yang dipakai sparse primary index untuk melompati data. Konfigurasi di atas disimpan sebagai config.d/memori.xml, dan validasi strukturnya dijalankan nyata:
import xml.etree.ElementTree as ET
import yaml
docker_compose = """
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
ulimits:
nofile: 262144
ports:
- "8123:8123"
- "9000:9000"
volumes:
- ./config.d:/etc/clickhouse-server/config.d
"""
config_mem = """
<clickhouse>
<max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
<mark_cache_size>536870912</mark_cache_size>
<timezone>Asia/Jakarta</timezone>
</clickhouse>
"""
hasil = yaml.safe_load(docker_compose)
print("services :", sorted(hasil["services"]))
print("port HTTP :", hasil["services"]["clickhouse"]["ports"][0])
print("port native :", hasil["services"]["clickhouse"]["ports"][1])
akar = ET.fromstring(config_mem)
print("memori.xml valid (ElementTree):")
for anak in akar:
print(f" {anak.tag} = {anak.text}")Output yang Dihasilkan:
services : ['clickhouse']
port HTTP : 8123:8123
port native : 9000:9000
memori.xml valid (ElementTree):
max_server_memory_usage_to_ram_ratio = 0.8
mark_cache_size = 536870912
timezone = Asia/JakartaPort 8123 melayani HTTP (Dashboard, clickhouse-client memakai port 9000 native). Daftar lengkap pengaturan server ada di dokumentasi ClickHouse
.