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:
-- "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á:
- WAL (Write-Ahead Log) zápis
- Index update (ak máte indexy — a máte)
- MVCC overhead (pre každý riadok sa ukladá viditeľnosť)
- 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
| Metrika | PostgreSQL | ClickHouse |
|---|---|---|
| INSERT rate | ~10 000/s | ~1 000 000+/s |
| Kompresia 1 TB logov | ~800 GB | ~50-100 GB |
| Agregácia nad 1B riadkami | minúty | pod 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)
# 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: 2621442. Tabuľky v ClickHouse
-- 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 mesiaceKľúčové rozdiely oproti PostgreSQL:
LowCardinality(String)— enum-like optimalizácia pre stĺpce s málo unikátnymi hodnotamiMergeTree()— engine optimalizovaný pre append-only workloadPARTITION BY— automatické rozdelenie dát po mesiacochTTL— automatické mazanie starých dát (žiadny cron job naDELETE FROM ... WHERE created_at < ...)
3. Zápis z aplikácie (Spring Boot / Node.js)
// 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
}
}// 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
-- 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.