Obsah článku / Contents
ClickHouse: Keď vaša PostgreSQL databáza kričí o pomoc kvôli logom

ClickHouse: Prestaňte trápiť PostgreSQL logmi a eventmi

Poznáte ten pocit, keď sa vaša aplikácia spomalí a vy otvoríte monitoring, len aby ste zistili, že PostgreSQL má 95% CPU utilization? A dôvod nie je zložitá business logika, ale to, že do jednej tabuľky events zapisujete tisíce riadkov za sekundu?

Toto je príbeh, ktorý som videl opakovane. A riešenie je prekvapivo jednoduché — prestaňte nútiť relačnú databázu robiť niečo, na čo nebola navrhnutá.

Problém: Relačná DB ako odpadkový kôš na eventy

Väčšina aplikácií začína jednoducho:

sql
-- "Daj to do PostgreSQL, veď to zvládne"
CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID NOT NULL,
    event_type VARCHAR(50) NOT NULL,
    payload JSONB,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE audit_logs (
    id BIGSERIAL PRIMARY KEY,
    entity_type VARCHAR(100),
    entity_id UUID,
    action VARCHAR(20),
    old_value JSONB,
    new_value JSONB,
    performed_by UUID,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE request_logs (
    id BIGSERIAL PRIMARY KEY,
    method VARCHAR(10),
    path TEXT,
    status_code INT,
    response_time_ms INT,
    ip_address INET,
    created_at TIMESTAMP DEFAULT NOW()
);

Na začiatku to funguje. 100 eventov za minútu? Žiadny problém. Ale potom príde rast:

Mesiac 1:  1 000 eventov/deň      → PostgreSQL: "Easy"
Mesiac 3:  50 000 eventov/deň     → PostgreSQL: "Zvládam"
Mesiac 6:  500 000 eventov/deň    → PostgreSQL: "Hmm..."
Mesiac 12: 5 000 000 eventov/deň  → PostgreSQL: "HELP"

Prečo PostgreSQL trpí?

PostgreSQL je row-oriented databáza optimalizovaná na:

  • ✅ ACID transakcie
  • ✅ Komplexné JOINy
  • ✅ UPDATE / DELETE operácie
  • ✅ Referenčnú integritu

Ale NIE na:

  • ❌ Masívny append-only zápis (milióny INSERTov)
  • ❌ Analytické query nad miliardami riadkov
  • ❌ Kompresiu stoviek GB logov
  • ❌ Agregácie typu "priemerný response time za posledných 30 dní po hodinách"

Každý INSERT do PostgreSQL znamená:

  1. WAL (Write-Ahead Log) zápis
  2. Index update (ak máte indexy — a máte)
  3. MVCC overhead (pre každý riadok sa ukladá viditeľnosť)
  4. VACUUM musí neskôr upratať mŕtve tuple

A toto všetko zaťažuje tú istú databázu, kde beží vaša business logika. Vaši zákazníci čakajú na checkout, pretože databáza je zaneprázdnená zapisovaním audit logov.

Riešenie: ClickHouse — databáza navrhnutá pre eventy

ClickHouse je open-source column-oriented analytická databáza, pôvodne vyvinutá v Yandexe. Je navrhnutá presne na to, čo PostgreSQL nezvláda:

Column-oriented vs Row-oriented

Row-oriented (PostgreSQL):
┌────┬──────────┬───────────┬─────────────┐
│ id │ user_id  │ event_type│ created_at  │ ← celý riadok uložený spolu
├────┼──────────┼───────────┼─────────────┤
│ 1  │ abc-123  │ click     │ 2026-02-01  │
│ 2  │ def-456  │ purchase  │ 2026-02-01  │
│ 3  │ abc-123  │ pageview  │ 2026-02-02  │
└────┴──────────┴───────────┴─────────────┘

Column-oriented (ClickHouse):
id:         [1, 2, 3, ...]           ← každý stĺpec uložený osobitne
user_id:    [abc-123, def-456, ...]
event_type: [click, purchase, ...]
created_at: [2026-02-01, 2026-02-01, ...]

Prečo je to rýchlejšie pre analytiku?

Keď spustíte query SELECT count(*) FROM events WHERE event_type = 'purchase', ClickHouse číta len stĺpec event_type — nie celé riadky. Pri miliardách riadkov je to rozdiel medzi sekundami a minútami.

Čísla, ktoré hovoria za seba

MetrikaPostgreSQLClickHouse
INSERT rate~10 000/s~1 000 000+/s
Kompresia 1 TB logov~800 GB~50-100 GB
Agregácia nad 1B riadkamiminútypod sekundu
Storage cost (1 rok logov)$$$$

Praktická implementácia: Hybridná architektúra

Cieľ nie je nahradiť PostgreSQL, ale odľahčiť ho. Business dáta zostávajú v PostgreSQL, eventy a logy idú do ClickHouse.

Architektúra

┌──────────────┐
                    │  Aplikácia   │
                    └──────┬───────┘
                           │
              ┌────────────┴────────────┐
              │                         │
              ▼                         ▼
    ┌─────────────────┐      ┌──────────────────┐
    │   PostgreSQL    │      │    ClickHouse     │
    │                 │      │                   │
    │ • Users         │      │ • Events          │
    │ • Orders        │      │ • Audit logs      │
    │ • Products      │      │ • Request logs    │
    │ • Transactions  │      │ • Metrics         │
    │                 │      │ • Analytics        │
    │ (Business data) │      │ (Append-only data)│
    └─────────────────┘      └──────────────────┘

1. ClickHouse setup (Docker)

yaml
# docker-compose.yml
services:
  clickhouse:
    image: clickhouse/clickhouse-server:latest
    ports:
      - "8123:8123"   # HTTP interface
      - "9000:9000"   # Native protocol
    volumes:
      - clickhouse-data:/var/lib/clickhouse
    environment:
      CLICKHOUSE_USER: admin
      CLICKHOUSE_PASSWORD: your-secure-password
    ulimits:
      nofile:
        soft: 262144
        hard: 262144

2. Tabuľky v ClickHouse

sql
-- Events tabuľka s MergeTree engine
CREATE TABLE events (
    event_id UUID DEFAULT generateUUIDv4(),
    user_id UUID,
    event_type LowCardinality(String),  -- optimalizácia pre opakujúce sa hodnoty
    payload String,                      -- JSON ako String (alebo použite JSON typ)
    created_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)       -- partície po mesiacoch
ORDER BY (event_type, created_at)        -- sort key pre rýchle query
TTL created_at + INTERVAL 12 MONTH;     -- automatické mazanie po roku

-- Audit log
CREATE TABLE audit_logs (
    entity_type LowCardinality(String),
    entity_id UUID,
    action LowCardinality(String),
    old_value String,
    new_value String,
    performed_by UUID,
    created_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (entity_type, created_at)
TTL created_at + INTERVAL 24 MONTH;

-- Request logs s automatickým TTL
CREATE TABLE request_logs (
    method LowCardinality(String),
    path String,
    status_code UInt16,
    response_time_ms UInt32,
    ip_address IPv4,
    created_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (status_code, created_at)
TTL created_at + INTERVAL 3 MONTH;      -- logy len 3 mesiace

Kľúčové rozdiely oproti PostgreSQL:

  • LowCardinality(String) — enum-like optimalizácia pre stĺpce s málo unikátnymi hodnotami
  • MergeTree() — engine optimalizovaný pre append-only workload
  • PARTITION BY — automatické rozdelenie dát po mesiacoch
  • TTL — automatické mazanie starých dát (žiadny cron job na DELETE FROM ... WHERE created_at < ...)

3. Zápis z aplikácie (Spring Boot / Node.js)

java
// Spring Boot — async event writer
@Service
public class ClickHouseEventWriter {

    private final ClickHouseClient client;
    private final BlockingQueue<Event> buffer = new LinkedBlockingQueue<>(10_000);

    @Scheduled(fixedRate = 1000) // Flush každú sekundu
    public void flushEvents() {
        List<Event> batch = new ArrayList<>();
        buffer.drainTo(batch, 5000); // Max 5000 eventov naraz

        if (batch.isEmpty()) return;

        // Batch INSERT — ClickHouse to miluje
        try (var insert = client.write("events")
                .format(ClickHouseFormat.JSONEachRow)) {
            for (Event event : batch) {
                insert.write(event.toJson());
            }
        }
    }

    public void track(Event event) {
        buffer.offer(event); // Non-blocking, nebrzdí hlavný request
    }
}
typescript
// Node.js — batch writer
import { createClient } from '@clickhouse/client';

const clickhouse = createClient({
  url: 'http://localhost:8123',
  username: 'admin',
  password: 'your-secure-password',
});

class EventBuffer {
  private buffer: Event[] = [];
  private readonly FLUSH_INTERVAL = 1000;
  private readonly BATCH_SIZE = 5000;

  constructor() {
    setInterval(() => this.flush(), this.FLUSH_INTERVAL);
  }

  track(event: Event) {
    this.buffer.push(event);
    if (this.buffer.length >= this.BATCH_SIZE) this.flush();
  }

  private async flush() {
    if (this.buffer.length === 0) return;
    const batch = this.buffer.splice(0, this.BATCH_SIZE);

    await clickhouse.insert({
      table: 'events',
      values: batch,
      format: 'JSONEachRow',
    });
  }
}

Dôležité: Vždy zapisujte v batch režime, nie po jednom riadku. ClickHouse je optimalizovaný na veľké batche — jeden INSERT s 5 000 riadkami je rádovo rýchlejší ako 5 000 jednotlivých INSERTov.

4. Query príklady — analytika, ktorá v PostgreSQL trvala minúty

sql
-- Top 10 najpomalších endpointov za posledných 7 dní
SELECT
    path,
    count() AS requests,
    avg(response_time_ms) AS avg_ms,
    quantile(0.95)(response_time_ms) AS p95_ms,
    quantile(0.99)(response_time_ms) AS p99_ms
FROM request_logs
WHERE created_at > now() - INTERVAL 7 DAY
GROUP BY path
ORDER BY p95_ms DESC
LIMIT 10;

-- Počet eventov po hodinách za posledný mesiac (pre dashboard)
SELECT
    toStartOfHour(created_at) AS hour,
    event_type,
    count() AS cnt
FROM events
WHERE created_at > now() - INTERVAL 30 DAY
GROUP BY hour, event_type
ORDER BY hour;

-- Error rate po dňoch
SELECT
    toDate(created_at) AS day,
    countIf(status_code >= 500) AS errors,
    count() AS total,
    round(errors / total * 100, 2) AS error_rate_pct
FROM request_logs
WHERE created_at > now() - INTERVAL 30 DAY
GROUP BY day
ORDER BY day;

Tieto query bežia nad miliardami riadkov za menej ako sekundu. Skúste to isté v PostgreSQL.

Kedy (ne)použiť ClickHouse

Použite ClickHouse keď:

  • ✅ Zapisujete veľa dát, ktoré sa nemenia (append-only)
  • ✅ Potrebujete analytiku nad veľkým objemom dát
  • ✅ Chcete ušetriť na storage (kompresia 10-20x)
  • ✅ Vaša PostgreSQL databáza je preťažená logmi/eventmi
  • ✅ Potrebujete automatický TTL na staré dáta

Nepoužívajte ClickHouse keď:

  • ❌ Potrebujete UPDATE/DELETE na jednotlivých riadkoch
  • ❌ Potrebujete ACID transakcie
  • ❌ Potrebujete JOINy medzi business entitami
  • ❌ Máte menej ako 100 000 eventov denne (PostgreSQL to zvládne)
  • ❌ Potrebujete point lookups (SELECT * FROM ... WHERE id = ?)

Záver: Správny nástroj na správnu prácu

Relačná databáza je výborná na to, na čo bola navrhnutá — business dáta, transakcie, referenčná integrita. Ale nútiť ju byť zároveň log storage, event store a analytický engine je ako nútiť chirurga, aby zároveň umýval podlahu v nemocnici.

Dajte PostgreSQL priestor na to, čo robí najlepšie. A eventy, logy a metriky pošlite tam, kde sa im bude dobre — do ClickHouse.

Všetky články