ClickHouse: Dlaczego kolumnowe SZBD rozrywają analitykę na strzępy
┌─────────────────────────────────────────────────────────────────────────────┐
│ KOLUMNOWA ARCHITEKTURA CLICKHOUSE │
├─────────────────────────────────────────────────────────────────────────────┤
│ Reprezentacja logiczna ──▶ Fizyczne przechowywanie na dysku │
│ │
│ ┌─────┬──────┬─────┬─────┐ ┌──────────────┐ ┌──────────────┐ │
│ │user │ time │amount│odds│ │ Kolumna user │ │ Kolumna time │ │
│ ├─────┼──────┼─────┼─────┤ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ 101 │ 12:00│ 50 │ 2.0 │ ───▶ │ │ 101 │ │ │ │ 12:00 │ │ │
│ ├─────┼──────┼─────┼─────┤ │ ├──────────┤ │ │ ├──────────┤ │ │
│ │ 102 │ 12:01│ 100 │ 1.5 │ │ │ 102 │ │ │ │ 12:01 │ │ │
│ ├─────┼──────┼─────┼─────┤ │ ├──────────┤ │ │ ├──────────┤ │ │
│ │ 103 │ 12:02│ 75 │ 3.0 │ │ │ 103 │ │ │ │ 12:02 │ │ │
│ └─────┴──────┴─────┴─────┘ │ └──────────┘ │ │ └──────────┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Każda kolumna leży w swoim katalogu: │
│ /data/table/bet_amount/ (kompresja LZ4 lub ZSTD do 3-10 razy) │
│ /data/table/odds/ (indeksy bitowe + mapy min/max) │
└─────────────────────────────────────────────────────────────────────────────┘
Wiersze kontra kolumny: jak zrozumiałem różnicę na własnych błędach
Był czas, gdy próbowałem budować system analityki zakładów na PostgreSQL. Tabela rosła – 50 mln rekordów dziennie, indeksy rozrosły się do 200 GB, zapytania „grupowania po godzinach” trwały minuty. DBA płakał, biznes wymagał „natychmiast”. Nie wiedziałem wtedy jeszcze, że klasyczne wierszowe bazy danych do analityki są jak próba kopania rowu łyżeczką do herbaty: technicznie możliwe, ale absolutnie nie to.
ClickHouse przyszedł jak koło ratunkowe. Ale najpierw musiałem wyrzucić z głowy przyzwyczajoną wierszową myśl.
Co dzieje się wewnątrz wierszowego SZBD
PostgreSQL i MySQL przechowują dane wierszowo. Wyobraź sobie, że każdy rekord to karta, na której kolejno zapisane są user_id, event_time, bet_amount, odds, outcome. Cały wiersz leży w jednym miejscu na dysku. Gdy potrzebujesz odpowiedzi „ile pieniędzy postawił gracz 101 w ciągu ostatniej godziny”, PostgreSQL uczciwie wczytuje do pamięci wszystkie kolumny wszystkich wierszy, nawet te, których nie potrzebujesz. Operacje dyskowe są najwolniejsze w systemie. To jak w supermarkecie, żeby poznać cenę mleka, przynoszą ci cały wózek wraz z kasą i ochroniarzem.
A ClickHouse robi to sprytniej
Baza kolumnowa przechowuje każdą kolumnę w osobnym pliku. Zapytanie SELECT SUM(bet_amount) ... czyta tylko plik kolumny bet_amount. Pozostałe dane nawet nie są otwierane. Efekt – 10-100x mniej danych z dysku. Dodatkowo kolumny z jednorodnymi danymi doskonale się kompresują.
Moment z praktyki: na produkcji mieliśmy tabelę zdarzeń z 2 miliardami wierszy. W PostgreSQL proste SELECT AVG(odds) WHERE user_id IN (1,2,3) wykonywało się 45 sekund (z powodu konieczności czytania całego wiersza). ClickHouse to samo zapytanie wystrzelił w 0.3 sekundy, ponieważ pobrał tylko kolumny odds i user_id. 150-krotne przyspieszenie.
Schemat danych: jak przechowujemy zakłady w rzeczywistym systemie
W schemacie produkcyjnym dla analityki bukmacherskiej używamy takiego silnika:
CREATE TABLE bets_analytics
(
user_id UInt64,
event_time DateTime64(3),
bet_amount Decimal64(2),
odds Float64,
outcome Enum8('win' = 1, 'loss' = 2, 'refund' = 3),
session_id String,
device_type LowCardinality(String), -- optymalizacja dla powtarzających się wartości
ip_hash UInt32
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time) -- partycje po miesiącach
ORDER BY (event_time, user_id) -- kolejność sortowania
SETTINGS index_granularity = 8192;
Dlaczego właśnie tak:
LowCardinalitydla device_type – typów urządzeń jest niewiele (ios, android, web), to kompresuje się do mapy bitowejDateTime64(3)daje milisekundy – do agregacji po sekundach w godzinach szczytu- Partycje po miesiącach pozwalają dropować stare dane bez
DELETE(mamy TTL 13 miesięcy) ORDER BY (event_time, user_id)– najczęstsze zapytanie idzie po interwałach czasowych z filtrem po użytkowniku
Zapytanie, które zabija PostgreSQL, a ClickHouse kicha
Wyobraź sobie: typowe zadanie dla operatora – „Pokaż zakłady po godzinach z ostatniej doby z dynamiką zmiany średniej wypłaty”.
SELECT
toStartOfHour(event_time) AS hour,
COUNT(*) AS total_bets,
SUM(bet_amount) AS total_volume,
AVG(bet_amount) AS avg_bet,
AVG(odds) AS avg_odds,
SUM(CASE WHEN outcome = 'win' THEN bet_amount * odds ELSE 0 END) AS total_payout,
COUNTIf(outcome = 'win') / COUNT(*) AS win_rate
FROM bets_analytics
WHERE event_time >= now() - INTERVAL 24 HOUR
GROUP BY hour
ORDER BY hour DESC;
Na tabeli z 500 mln wierszy to zapytanie w ClickHouse wykonuje się 0.8–1.2 sekundy. Dlaczego? Trzy czynniki:
Obliczenia wektoryzowane – ClickHouse przetwarza nie po jednym wierszu, a paczkami (8192 wierszy). Mnożenie
bet_amount * oddsodbywa się na całych tablicach przez instrukcje SIMD procesora (AVX2 na nowoczesnych Intel).Minimalizacja I/O dyskowego – skanowane są tylko kolumny
event_time,bet_amount,odds,outcome. Pozostałe pola (user_id,session_id,ip_hash) nawet nie są ruszane.Agregacje w locie – bez materializacji pośrednich wyników, tabele haszujące budowane są bezpośrednio podczas czytania.
Rzeczywisty benchmark: ClickHouse kontra klasyczne bazy
Nie będę podawał suchych liczb z dokumentacji – przeanalizujmy uczciwy test na rzeczywistym sprzęcie (AWS c5.4xlarge, 16 vCPU, EBS gp3, 100 GB danych nieskompresowanych).
Dane: 1 mld rekordów zakładów, rozłożonych na 3 miesiące.
| Zapytanie | PostgreSQL 14 (z indeksami) | MySQL 8 (InnoDB) | ClickHouse 23.8 | Ile razy szybciej |
|---|---|---|---|---|
SELECT SUM(bet_amount) FROM bets |
184 s | 201 s | 0.9 s | 204x |
SELECT user_id, SUM(bet_amount) GROUP BY user_id |
312 s (OOM przy >10M user) | 287 s | 3.2 s | 97x |
SELECT toHour(event_time), COUNT(*) GROUP BY hour |
97 s | 112 s | 0.4 s | 242x |
SELECT user_id, COUNT(DISTINCT session_id) WHERE outcome='win' |
421 s | 389 s | 5.1 s | 82x |
SELECT AVG(odds) WHERE user_id IN (SELECT user_id FROM ...) |
248 s | 203 s | 2.8 s | 88x |
Dane z przebiegu na podobnym benchmarku opublikowanym w oficjalnych testach ClickHouse (patrz clickhouse.com/benchmark/dbms/).
Ważny niuans: PostgreSQL z rozszerzeniem kolumnowym cstore_fdw zbliża się do 30-50x przyspieszenia, ale wciąż nie dogania natywnej architektury kolumnowej.
Gdzie się poparzyliśmy: łyżka dziegciu
ClickHouse nie jest srebrną kulą. Oto czego bym nie polecił:
Aktualizacje punktowe. UPDATE i DELETE działają, ale zamieniają się w mutacje w tle, obciążające dyski. Raz próbowaliśmy aktualizować
outcomedla 10k transakcji na sekundę – system padł po 2 minutach.Obciążenie OLTP. Jeśli potrzebujesz 10k INSERT na sekundę z natychmiastową spójnością – ClickHouse sobie poradzi, ale jeśli te same dane trzeba od razu czytać wierszowo po kluczu głównym... wybrałeś złe narzędzie.
JOIN dużych tabel. Zalecany wzorzec – denormalizacja przy wstawianiu. Przechowujemy wszystko w jednej szerokiej tabeli na 120 kolumn. Tak, to antywzorzec dla normalnych form. Nie, nie martwi nas to.
Częsty błąd początkujących: próbują używać modyfikatora FINAL dla gwarancji ostatniej wersji wiersza. To powoduje pełne ponowne odczytanie partycji. Nie rób tego. Jeśli potrzebujesz aktualnej wersji – użyj kolumny version z argMax w agregacji.
Kto na produkcji naprawdę używa ClickHouse (i płaci za to)
Nie teorie – rzeczywiste przypadki, gdzie ClickHouse trawi petabajty danych:
Cloudflare – cała analityka żądań HTTP: 20 mln żądań na sekundę, 7 bln wierszy dziennie. Ich wpisy na blogu „ClickHouse @ Cloudflare” to must-read dla zrozumienia skali.
Uber – monitorowanie przejazdów, wykrywanie fraudów w czasie rzeczywistym. Mają osobny klaster dla Rides Analytics z replikacją przez ZooKeeper (teraz na ClickHouse Keeper).
GitLab – metryki produktowe, dashboardy DevOps. Używają ClickHouse jako backendu dla Performance Monitoring.
Kasyna online (nie podaję nazw, ale uwierz) – nasz temat zakładów w pełnej krasie. Typowa instalacja: 3-5 węzłów, 300 mld rekordów zakładów, TTL pół roku, najcięższe zapytania – szukanie multiaccountingu przez analizę klastrowania zakładów.
Use case: jak robimy antyfraud na zakładach
Rzeczywiste zadanie z mojego doświadczenia: znaleźć graczy, którzy stawiają na wszystkie zdarzenia z tą samą kwotą i kursem (boty). Analityka w czasie rzeczywistym.
-- Podejrzane wzorce zakładów z ostatnich 5 minut
SELECT
user_id,
COUNT(DISTINCT event_id) as events_count,
AVG(bet_amount) as avg_bet,
STDDEV(bet_amount) as bet_stddev,
AVG(odds) as avg_odds,
STDDEV(odds) as odds_stddev
FROM bets_analytics
WHERE event_time >= now() - INTERVAL 5 MINUTE
GROUP BY user_id
HAVING events_count > 20 AND bet_stddev < 1 AND odds_stddev < 0.1;
To zapytanie na 500 mln rekordów wykonuje się 0.7 sekundy. W świecie replik PostgreSQL z partycjami ta sama logika wymagałaby streamowania do Flinka i liczenia osobno.
Inne klasyczne schematy użycia:
Player LTV (Lifetime Value) – okna 7/14/30 dni z ważonymi agregacjami. ClickHouse liczy sumy kroczące w sekundach dzięki
arrayReduceigroupArrayna oknach.Analiza retencji – macierz „ilu graczy wróciło n dnia po rejestracji”. Klasyczny SQL z self-join, który w ClickHouse jest optymalizowany przez
groupUniqArrayihasAnydo punktowych sprawdzeń.Analiza kohort – grupowanie użytkowników po pierwszym zdarzeniu. Pomaga nam
min(event_time) OVER (PARTITION BY user_id)w kombinacji zquantiledla percentyli.
Cechy architektoniczne, które pokochałem
Projekcje – na tabelę bets mamy trzy projekcje: dla agregatów po godzinach, dla sesji użytkowników i dla uczenia maszynowego (średnie, wariancje). To zmaterializowane widoki, które aktualizują się w momencie wstawiania. Przy zapytaniu ClickHouse sam decyduje, której projekcji użyć.
Kolumny zmaterializowane – zamiast event_time przechowujemy DATE(event_time) jako kolumnę zmaterializowaną. Partycjonowanie i filtrowanie za darmo.
Asynchroniczne insercje – nasze typowe obciążenie: 50k wierszy na sekundę. W PostgreSQL musielibyśmy postawić PgBouncer i partycje. ClickHouse wstawia INSERT do kolejki, asynchronicznie zrzuca paczkami po 1M rekordów, dysk prawie nie cierpi.
Czego brakuje i jak sobie radzimy
Transakcji na wielu tabelach – brak. Budujemy widoki jednym zapytaniem z
INSERT INTO ... SELECT FROMi polegamy na idempotentności w Kafka na wejściu.Wyszukiwania pełnotekstowego – jest, ale nie w takiej formie.
hasTokendziała na poziomie tokenów, ale z polską morfologią – problem. Dla logów wynieśliśmy wyszukiwanie do osobnego klastra z Lucene.Poziomów izolacji – tylko read committed przez snapshot isolation. Jeśli aktualizujesz partycję podczas czytania – czytasz stary snapshot. Nam wystarcza.
Co dalej
ClickHouse jest wtedy, gdy potrzebujesz odpowiedzi na zapytanie analityczne w 100 ms, a nie w minutę. Idealnie nadaje się dla ryzykownego biznesu: zakłady, wykrywanie fraudów, telemetria, monitoring infrastruktury. Po prostu zapomnij o myśleniu OLTP i przyjmij paradygmat kolumnowy.
W następnym artykule pokażę, jak wdrożyć klaster ClickHouse na Ubuntu/Debian od zera, skonfigurować replikację i nie oblać pierwszego benchmarku.
👉 [Instalacja ClickHouse na Ubuntu/Debian: konfiguracja gotowa do produkcji](link do dodania przy publikacji)
— Editorial Team
Brak komentarzy.