Powrót do strony głównej

ClickHouse: dlaczego kolumnowe bazy danych przyspieszają analitykę 100 razy

Artykuł wyjaśnia zasadniczą różnicę między kolumnowymi a wierszowymi bazami danych na przykładzie analityki zakładów. Przedstawiono rzeczywiste benchmarki ClickHouse vs PostgreSQL i MySQL z przyspieszeniem do 242 razy, schemat architektury przechowywania, działające zapytania SQL dla LTV i wykrywania oszustw, a także uczciwe ograniczenia technologii od inżyniera z doświadczeniem produkcyjnym.

ClickHouse vs PostgreSQL: 200x przyspieszenie w analityce zakładów
Advertisement 728x90

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.

Google AdInline article slot

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:

Google AdInline article slot
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:

  • LowCardinality dla device_type – typów urządzeń jest niewiele (ios, android, web), to kompresuje się do mapy bitowej
  • DateTime64(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:

Google AdInline article slot
  1. Obliczenia wektoryzowane – ClickHouse przetwarza nie po jednym wierszu, a paczkami (8192 wierszy). Mnożenie bet_amount * odds odbywa się na całych tablicach przez instrukcje SIMD procesora (AVX2 na nowoczesnych Intel).

  2. 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.

  3. 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ć outcome dla 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 arrayReduce i groupArray na 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 groupUniqArray i hasAny do punktowych sprawdzeń.

  • Analiza kohort – grupowanie użytkowników po pierwszym zdarzeniu. Pomaga nam min(event_time) OVER (PARTITION BY user_id) w kombinacji z quantile dla 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 FROM i polegamy na idempotentności w Kafka na wejściu.

  • Wyszukiwania pełnotekstowego – jest, ale nie w takiej formie. hasToken dział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)


Następny:

— Editorial Team

Advertisement 728x90

Czytaj dalej