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:

sql
-- 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:

sql
-- 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:

yaml
services:
  clickhouse:
    image: clickhouse/clickhouse-server:latest
    ulimits:
      nofile: 262144
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - ./config.d:/etc/clickhouse-server/config.d
xml
<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:

python
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:

text
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/Jakarta

Port 8123 melayani HTTP (Dashboard, clickhouse-client memakai port 9000 native). Daftar lengkap pengaturan server ada di dokumentasi ClickHouse .