Modelovanie eventov v ClickHouse vyžaduje pochopenie jeho unikátnej architektúry. Na rozdiel od tradičných riadkovo orientovaných databáz je ClickHouse stĺpcová OLAP databáza optimalizovaná pre analytické dotazy nad veľkými datasetmi. Správny návrh tabuliek od začiatku určuje, či vaše dotazy vrátia výsledky v milisekundách alebo minútach.
Tento sprievodca pokrýva základné koncepty pre modelovanie event dát v ClickHouse, od tabuľkových engineov až po stratégie optimalizácie dotazov.
Pochopenie ClickHouse Table Engines
ClickHouse ponúka viacero tabuľkových engineov, ale pre event analytiku je rodina MergeTree nevyhnutná. Každý variant enginu slúži pre rôzne prípady použitia.
MergeTree
Základný engine pre väčšinu analytických workloadov. Poskytuje:
- Indexovanie primárneho kľúča: Riedky index, ktorý ukladá pointre na granuly (bloky 8,192 riadkov štandardne) pre rýchle lokalizovanie dát
- Partitioning dát: Fyzické oddelenie dát podľa partition kľúča
- Kompresia dát: Kompresia na úrovni stĺpcov s rôznymi kodekmi
- Merge na pozadí: Automatická optimalizácia dátových častí
CREATE TABLE events (
event_id UUID,
event_time DateTime,
event_date Date,
user_id String,
event_type LowCardinality(String),
properties String,
processed_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_type, user_id, event_time)
TTL event_date + INTERVAL 2 YEAR;
ReplacingMergeTree
Použite keď potrebujete deduplikovať dáta na základe sorting kľúča. Dôležité upozornenia na pochopenie:
- Deduplikácia prebieha počas background merge operácií, nie pri inserte
- Kým merge nie je dokončený, duplicitné riadky sa môžu objaviť vo výsledkoch dotazov
- Použite modifikátor
FINALna SELECT pre získanie deduplikovaných výsledkov počas dotazu - Deduplikácia neprebieha naprieč rôznymi partíciami
- Version stĺpec pomáha určiť, ktorý riadok zachovať (najvyššia verzia vyhráva)
CREATE TABLE user_profiles (
user_id String,
updated_at DateTime,
email String,
plan LowCardinality(String),
properties String
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;
-- Dotaz s FINAL pre zabezpečenie deduplikácie
SELECT * FROM user_profiles FINAL WHERE user_id = 'user_123';
AggregatingMergeTree
Predagreguje dáta počas merge operácií pre rýchlejšie analytické dotazy. Vyžaduje špeciálne zaobchádzanie:
- Používajte
AggregateFunctiondátové typy pre ukladanie medzistupňov agregácie - Vkladajte dáta pomocou
-Statevariantov agregačných funkcií (napr.sumState,uniqState) - Dotazujte dáta pomocou
-Mergevariantov funkcií (napr.sumMerge,uniqMerge) - Najlepšie keď redukuje počet riadkov o rádovo
- Bežne používané s materialized views pre real-time dashboardy
-- Cieľová tabuľka pre agregované dáta
CREATE TABLE events_hourly (
hour DateTime,
event_type LowCardinality(String),
event_count SimpleAggregateFunction(sum, UInt64),
unique_users AggregateFunction(uniq, String)
)
ENGINE = AggregatingMergeTree()
ORDER BY (event_type, hour);
-- Materialized view na jej naplnenie
CREATE MATERIALIZED VIEW events_hourly_mv TO events_hourly AS
SELECT
toStartOfHour(event_time) AS hour,
event_type,
count() AS event_count,
uniqState(user_id) AS unique_users
FROM events
GROUP BY hour, event_type;
-- Dotaz pomocou -Merge funkcií
SELECT hour, event_type, sum(event_count), uniqMerge(unique_users)
FROM events_hourly
GROUP BY hour, event_type;
Návrh primárneho kľúča a poradia triedenia
Klauzula ORDER BY definuje fyzické poradie triedenia dát na disku. Štandardne sa stáva aj primárnym kľúčom (riedky index). Voliteľne môžete definovať samostatný PRIMARY KEY, ktorý musí byť prefixom ORDER BY výrazu.
Princípy výberu kľúča
- Stĺpce s nízkou kardinalitou najprv: Stĺpce s menej unikátnymi hodnotami by mali ísť pred stĺpce s vysokou kardinalitou, pretože to maximalizuje efektivitu indexu
- Zodpovedajte vašim query patternom: Najľavejšie stĺpce v ORDER BY by mali byť vaše najbežnejšie filter podmienky
- Použite 3-5 stĺpcov: Dobrý ORDER BY má typicky 3-5 stĺpcov; viac zriedka zlepšuje výkon
- Umiestnenie timestampu: Existujú dva bežné patterny:
(..., timestamp)– timestamp posledný, pre dotazy ktoré filtrujú najprv podľa iných dimenzií(..., toStartOfDay(timestamp), ..., timestamp)– zaokrúhlený čas v strede, pre dotazy pristupujúce k malým časovým rozsahom v rámci väčších partícií
- Zvážte kompresiu: Pre clickstream dáta môže zoradenie podľa
session_idzlepšiť kompresiu aj keď má vyššiu kardinalitu, pretože súvisiace eventy sa zoskupia
Príklad: Tabuľka pre event analytiku
-- Dobre: Nízka kardinalita najprv, zodpovedá bežným dotazom
ORDER BY (event_type, user_id, event_time)
-- Dotaz, ktorý benefituje z tohto poradia:
SELECT count(*) FROM events
WHERE event_type = 'Page Viewed'
AND user_id = 'user_123'
AND event_time >= '2025-01-01';
-- Samostatný PRIMARY KEY pre menší index (pokročilý prípad)
CREATE TABLE events (...)
ENGINE = MergeTree()
ORDER BY (event_type, user_id, event_time)
PRIMARY KEY (event_type, user_id);
Bežné anti-patterny
- UUID najprv: Umiestnenie stĺpcov s vysokou kardinalitou ako UUID na začiatok ničí efektivitu indexu
- Iba timestamp:
ORDER BY event_timesamotné limituje možnosti filtrovania pre ne-časové dotazy - Príliš veľa stĺpcov: Viac ako 5 stĺpcov zriedka zlepšuje výkon a zväčšuje veľkosť indexu
- Preskakovanie najľavejších stĺpcov: Dotazy filtrujúce podľa stĺpcov, ktoré nie sú na začiatku ORDER BY, nemôžu efektívne využiť riedky index
Stratégie partitioningu
Partitioning fyzicky oddeľuje dáta do nezávislých častí. Dôležité: Vnímajte partitioning primárne ako nástroj na správu dát, nie ako funkciu optimalizácie dotazov. Časti v rôznych partíciách sa nikdy nezlučujú.
Časový partitioning
Väčšina event dát benefituje z mesačných partícií:
-- Mesačné partície (odporúčané pre väčšinu prípadov)
PARTITION BY toYYYYMM(event_date)
-- Denné partície (scenáre s vysokým objemom, krátka retencia)
PARTITION BY event_date
-- Týždenné partície
PARTITION BY toMonday(event_date)
-- Ročné partície (dlhá retencia, nižší objem)
PARTITION BY toYear(event_date)
Pokyny pre veľkosť partícií
- Cieľová veľkosť: 1-300 GB na partíciu (nie počet riadkov)
- Počet partícií: Udržujte v desiatkach až stovkách, nie v tisícoch
- Príliš veľa partícií: Spôsobuje "Too many parts" chyby, pretože časti sa nezlučujú naprieč partíciami
- Malé tabuľky: Tabuľky pod 10 GB typicky nepotrebujú partitioning vôbec
- Prístup dotazov: SELECT dotaz by nemal pristupovať k viac ako niekoľkým desiatkam partícií
Multi-dimenzionálny partitioning
Pre multi-tenant scenáre kombinujte čas s tenantom (používajte opatrne aby ste sa vyhli príliš veľkému počtu partícií):
-- Iba ak workspace_id má nízku kardinalitu (desiatky, nie tisíce)
PARTITION BY (workspace_id, toYYYYMM(event_date))
Kompresia a dátové typy
Výber správnych dátových typov a kompresných kodekov významne ovplyvňuje úložisko a výkon dotazov.
Optimálne dátové typy
- LowCardinality(String): Pre stĺpce s menej ako ~10,000 unikátnymi hodnotami (typy eventov, krajiny, stavové kódy). Výkon sa môže zhoršiť pri viac ako 100,000 distinct hodnôt
- Enum8/Enum16: Pre fixné, známe sety hodnôt, ktoré sa zriedka menia. Efektívnejšie na úložisko než LowCardinality ale vyžaduje zmeny schémy pre pridanie hodnôt
- FixedString(N): Pre hodnoty s pevnou dĺžkou ako UUID (36 znakov) alebo ISO kódy krajín
- DateTime vs DateTime64: Použite DateTime64 len keď potrebujete sub-sekundovú presnosť
- Nullable: Vyhnite sa keď je to možné; používajte namiesto toho default hodnoty. Nullable stĺpce vyžadujú dodatočný stĺpec pre sledovanie null stavov, čo ovplyvňuje výkon
- Najmenší integer typ: Používajte UInt8, UInt16, atď. podľa skutočných rozsahov hodnôt, nie pre pohodlie
Kompresné kodeky
ClickHouse používa LZ4 štandardne (ZSTD je predvolený v ClickHouse Cloud). Špecializované kodeky môžu byť kombinované s general-purpose kompresiou:
CREATE TABLE events (
event_id UUID CODEC(ZSTD(1)),
event_time DateTime CODEC(Delta, ZSTD(1)),
sequence_id UInt64 CODEC(DoubleDelta, LZ4),
user_id String CODEC(ZSTD(3)),
event_type LowCardinality(String),
metrics Float64 CODEC(Gorilla, ZSTD(1)),
flags UInt8 CODEC(T64, ZSTD(1))
);
Odporúčania pre kodeky:
- Timestamps: Delta + ZSTD pre vynikajúcu kompresiu na sekvenčných časoch
- Monotónne integery: DoubleDelta + LZ4 pre countery alebo sekvenčné ID
- Floats/metriky: Gorilla + ZSTD pre time-series gauge dáta s malými zmenami
- Stringy: ZSTD s úrovňou kompresie 1-3 (vyššie úrovne zriedka poskytujú významné zlepšenie)
- Malé integery: T64 + ZSTD keď hodnoty využívajú len časť rozsahu typu
- Neznáme patterny: ZSTD samotné je silný general-purpose default
Materialized Views pre predagregáciu
ClickHouse podporuje dva typy materialized views: inkrementálne (real-time, spúšťané na INSERT) a refreshable (plánovaný periodický refresh). Inkrementálne views sú preferované pre väčšinu prípadov.
Vytvorenie inkrementálneho Materialized View
-- Zdrojová tabuľka prijíma raw eventy
CREATE TABLE events_raw (
event_time DateTime,
user_id String,
event_type LowCardinality(String),
page_path String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, event_time);
-- Explicitne najprv vytvorte cieľovú tabuľku (odporúčané)
CREATE TABLE events_daily (
date Date,
event_type LowCardinality(String),
event_count UInt64,
unique_users UInt64
) ENGINE = SummingMergeTree()
ORDER BY (date, event_type);
-- Materialized view s TO klauzulou
CREATE MATERIALIZED VIEW events_daily_mv
TO events_daily
AS SELECT
toDate(event_time) AS date,
event_type,
count() AS event_count,
uniqExact(user_id) AS unique_users
FROM events_raw
GROUP BY date, event_type;
Refreshable Materialized Views
Pre komplexné dotazy, joiny naprieč viacerými tabuľkami, alebo keď je mierna neaktuálnosť akceptovateľná:
CREATE MATERIALIZED VIEW top_pages_mv
REFRESH EVERY 1 HOUR
TO top_pages
AS SELECT
page_path,
count() AS views
FROM events_raw
WHERE event_time >= now() - INTERVAL 24 HOUR
GROUP BY page_path
ORDER BY views DESC
LIMIT 100;
Osvedčené postupy
- Používajte klauzulu
TOpre špecifikovanie explicitnej cieľovej tabuľky pre lepšiu kontrolu a udržiavateľnosť - Vyhnite sa POPULATE pri vytváraní views na veľkých tabuľkách – môže preskočiť riadky vložené počas populácie. Namiesto toho backfillujte manuálne po vytvorení
- Udržujte transformácie jednoduché aby ste sa vyhli bottleneckom pri ingescii
- View sa spúšťa len na INSERTy do najľavejšej tabuľky v SELECTe – nereaguje na updates alebo deletes
- Používajte SummingMergeTree pre sum/count agregácie, AggregatingMergeTree pre komplexné agregácie
- Monitorujte lag materialized views počas období s vysokým objemom cez
system.query_views_log
Data Skipping Indexes
Okrem primárneho kľúča ClickHouse podporuje sekundárne data skipping indexy, ktoré pomáhajú filtrovať granuly pre stĺpce mimo ORDER BY:
CREATE TABLE events (
event_id UUID,
event_time DateTime,
user_id String,
event_type LowCardinality(String),
error_code Nullable(UInt16),
request_id String,
INDEX idx_error_code error_code TYPE minmax GRANULARITY 4,
INDEX idx_request_id request_id TYPE bloom_filter(0.01) GRANULARITY 4
) ENGINE = MergeTree()
ORDER BY (event_type, event_time);
Typy indexov:
- minmax: Ukladá min/max hodnoty na skupinu granúl. Dobrý pre range dotazy
- set(N): Ukladá až N unikátnych hodnôt na skupinu granúl
- bloom_filter: Pravdepodobnostný filter pre point lookups na stĺpcoch s vysokou kardinalitou
- ngrambf_v1: N-gram bloom filter pre LIKE dotazy na stringoch
Techniky optimalizácie dotazov
Aj s dobrým návrhom tabuliek záleží na optimalizácii dotazov. Tu sú kľúčové techniky pre analytické dotazy.
Použite PREWHERE pre náročné filtre
PREWHERE filtruje dáta pred čítaním ostatných stĺpcov, čo redukuje I/O. ClickHouse to často aplikuje automaticky, ale môžete to urobiť explicitne:
-- PREWHERE filtruje pred čítaním ostatných stĺpcov
SELECT user_id, properties
FROM events
PREWHERE event_type = 'Purchase Completed'
WHERE toDate(event_time) >= '2025-01-01';
Použite FINAL pre ReplacingMergeTree
Pri dotazovaní ReplacingMergeTree tabuliek použite FINAL pre zabezpečenie deduplikovaných výsledkov:
-- Bez FINAL: môže vrátiť duplicitné riadky
SELECT * FROM user_profiles WHERE user_id = 'user_123';
-- S FINAL: garantovaná deduplikácia (pomalšie)
SELECT * FROM user_profiles FINAL WHERE user_id = 'user_123';
Sampling pre približné výsledky
-- Rýchle približné počty na veľkých datasetoch
SELECT event_type, count() * 10 AS estimated_count
FROM events SAMPLE 0.1
GROUP BY event_type;
Aproximačné vs exaktné funkcie
-- Exaktné (pomalšie, viac pamäte)
SELECT uniqExact(user_id) FROM events;
-- Aproximačné (10-100x rýchlejšie, ~2% margin chyby)
SELECT uniq(user_id) FROM events;
Vyhýbanie sa bežným výkonovým problémom
- SELECT *: Vždy špecifikujte len potrebné stĺpce
- DISTINCT na veľkých množinách: Používajte
uniq()alebouniqExact()namiesto toho - ORDER BY bez LIMIT: Vždy párujte triedenie s limitmi
- Funkcie na indexovaných stĺpcoch:
WHERE toDate(event_time) = '2025-01-01'bráni použitiu indexu; preferujteWHERE event_time >= '2025-01-01' AND event_time < '2025-01-02' - Nullable stĺpce v GROUP BY: Vyhnite sa keď je to možné; ovplyvňuje výkon agregácie
Efektívne joiny
-- Používajte dictionaries pre dimension lookups (najrýchlejšie)
SELECT
e.event_type,
dictGet('users_dict', 'email', e.user_id) AS user_email
FROM events e
WHERE e.event_time >= today() - 7;
-- Pre veľké joiny dajte menšiu tabuľku na pravú stranu
SELECT e.*, u.plan
FROM events e
INNER JOIN users u ON e.user_id = u.user_id;
-- Zvážte denormalizáciu pre read-heavy workloady
Projekcie ako alternatíva
Projekcie ukladajú predpočítané výsledky dotazov v rámci rovnakej tabuľky, automaticky vyberané optimizátorom dotazov:
CREATE TABLE events (
event_time DateTime,
user_id String,
event_type LowCardinality(String),
page_path String,
-- Definujte projekciu pre dotazy zoskupené podľa používateľa
PROJECTION events_by_user (
SELECT * ORDER BY user_id, event_time
)
) ENGINE = MergeTree()
ORDER BY (event_type, event_time);
Projekcie vs. Materialized Views:
- Projekcie sú uložené v rámci rovnakej tabuľky a automaticky udržiavané
- Optimizátor automaticky vyberá projekcie keď je to výhodné
- Materialized views ponúkajú viac flexibility a môžu transformovať dáta
- Projekcie pridávajú storage overhead proporcionálne k dátam, ktoré ukladajú
Stratégie evolúcie schémy
Event schémy sa časom vyvíjajú. Plánujte zmeny od začiatku.
Pridávanie stĺpcov
-- Pridajte nový stĺpec s default hodnotou
ALTER TABLE events
ADD COLUMN campaign_id String DEFAULT '';
-- Doplňte dáta ak je to potrebné (mutácia - používajte opatrne)
ALTER TABLE events
UPDATE campaign_id = extractURLParameter(page_url, 'utm_campaign')
WHERE campaign_id = '';
Flexibilný stĺpec properties
Ukladajte variabilné properties v JSON stĺpci pre flexibilitu:
CREATE TABLE events (
event_id UUID,
event_time DateTime,
event_type LowCardinality(String),
-- Štruktúrované polia pre bežné dotazy
user_id String,
page_path String,
-- Flexibilný JSON pre variabilné properties
properties String
) ENGINE = MergeTree()
ORDER BY (event_type, event_time);
-- Dotaz na JSON properties
SELECT JSONExtractString(properties, 'browser') AS browser
FROM events
WHERE event_type = 'Page Viewed';
Monitoring a údržba
Udržujte vaše ClickHouse tabuľky zdravé s pravidelným monitoringom.
Kľúčové metriky na sledovanie
- Počet aktívnych častí:
SELECT count() FROM system.parts WHERE active AND table = 'events' - Veľkosti partícií: Monitorujte nerovnomerné distribúcie
- Výkon dotazov: Používajte
system.query_logpre analýzu pomalých dotazov - Zdravie merge: Zabezpečte, že background merge drží krok s insertmi
- Kompresné pomery: Kontrolujte cez
system.parts_columns
Údržbové úlohy
-- Vynúťte merge malých častí (používajte opatrne, resource-intenzívne)
OPTIMIZE TABLE events FINAL;
-- Zahoďte staré partície (rýchla operácia)
ALTER TABLE events DROP PARTITION '202301';
-- Skontrolujte zdravie tabuľky
SELECT
table,
partition,
count() AS parts,
sum(rows) AS rows,
formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.parts
WHERE active AND database = 'default'
GROUP BY table, partition
ORDER BY parts DESC;
Zhrnutie
Efektívne modelovanie event dát v ClickHouse vyžaduje:
- Výber správneho variantu MergeTree enginu (MergeTree pre raw dáta, ReplacingMergeTree pre deduplikáciu, AggregatingMergeTree pre predagregáciu)
- Návrh ORDER BY s 3-5 stĺpcami, nízka kardinalita najprv, zodpovedajúci vašim query patternom
- Partitioning podľa času pre správu dát (desiatky až stovky partícií, nie tisíce)
- Používanie optimálnych dátových typov (LowCardinality, najmenšie integer typy, vyhýbanie sa Nullable)
- Aplikovanie vhodných kompresných kodekov (Delta pre timestamps, ZSTD ako general-purpose)
- Využívanie materialized views pre predagregáciu, s explicitnými TO tabuľkami
- Pochopenie, že ReplacingMergeTree vyžaduje FINAL pre garantovanú deduplikáciu
- Aplikovanie techník optimalizácie dotazov (PREWHERE, aproximačné funkcie, správny výber stĺpcov)
Investujte čas do návrhu na začiatku, testujte s realistickými objemami dát a iterujte na základe skutočných query patternov. Dobre namodelovaná ClickHouse databáza zvládne miliardy eventov a pritom bude vracať výsledky v milisekundách.