Powrót do strony głównej

Specjalne silniki ClickHouse: kiedy MergeTree nie jest potrzebny

Artykuł opisuje specjalne silniki ClickHouse dla zadań, gdzie standardowy MergeTree jest nieoptymalny: Memory dla danych tymczasowych i pamięci podręcznej, Buffer dla buforowania wysokoczęstotliwościowych wstawek (ochrona przed Too many parts), Null dla organizacji przetwarzania strumieniowego przez Materialized Views, rodzina Log dla małych słowników, URL/File/S3 dla danych zewnętrznych, PostgreSQL ENGINE dla dostępu na żywo. Przedstawiono wzorzec architektoniczny Kafka → Buffer → MergeTree dla 10k+ zdarzeń na sekundę.

Specjalne silniki ClickHouse: Memory, Buffer, Null i inne
Advertisement 728x90

Specjalne silniki ClickHouse: gdy MergeTree nie pasuje

Wyobraź sobie, że musisz szybko przetworzyć paczkę zakładów — pogrupować, policzyć sumy częściowe, a potem wysłać do głównej tabeli. Nie chcesz zapisywać na dysk, bo dane są tymczasowe i potrzebne tylko na czas zapytania.

Memory ENGINE przechowuje dane w całości w pamięci RAM. To najszybszy silnik — brak operacji dyskowych, brak kompresji, brak indeksów (poza kluczem głównym). Jest jednak cena: po restarcie ClickHouse tabela staje się pusta. Dane NIE są przechowywane.

Kiedy używać:

Google AdInline article slot
  • Tabele stagingowe (pośrednie) dla procesów ETL. Na przykład załadowałeś milion zakładów z Kafka, zrobiłeś deduplikację, a dopiero potem wstawiłeś do głównej tabeli MergeTree.
  • Cache dla live odds — kursy zmieniają się co sekundę, nie trzeba przechowywać historii, potrzebny jest tylko bieżący snapshot.
  • Małe słowniki (do 10–15 mln rekordów), które są odtwarzane przy każdym uruchomieniu skryptu.

Przykład dla cache live kursów:

-- Tabela dla aktualnych kursów (żyje w RAM)
CREATE TABLE live_odds_cache
(
    event_id     UInt64,          -- ID wydarzenia (mecz)
    market_id    UInt32,          -- ID rynku zakładów
    selection_id UInt32,          -- ID wyniku
    odds         Decimal(10,3),   -- Kurs
    updated_at   DateTime
)
ENGINE = Memory()
ORDER BY (event_id, market_id, selection_id);   -- ORDER BY obowiązkowy, ale indeks nieefektywny

Wstawiamy dane (np. ze strumienia):

-- Przyszła nowa notowania, wstawiamy
INSERT INTO live_odds_cache VALUES (100500, 10, 200, 1.85, now());

-- Odczytujemy aktualny kurs dla zakładu
SELECT odds FROM live_odds_cache 
WHERE event_id = 100500 AND market_id = 10 AND selection_id = 200;

Pułapki:

Google AdInline article slot
  • Tabela Memory nie obsługuje scalania (merge) — jeśli robisz wiele UPDATE (przez wstawianie z anulowaniem), pamięć będzie puchnąć. Użyj TRUNCATE do czyszczenia.
  • Po restarcie ClickHouse dane są tracone. Nie przechowuj tu niczego krytycznego.
  • Rozmiar tabeli jest ograniczony dostępną pamięcią RAM. Jeśli tabela urośnie do 50 GB na serwerze z 64 GB RAM — serwer padnie.

Analogia: Memory ENGINE to jak tablica suchościeralna. Szybko piszesz, szybko czytasz, ale po sprzątaczce (restart) tablica jest pusta.

2. Buffer ENGINE — buforowanie INSERT przed zapisem

Masz 10 000 zakładów na sekundę. Każdy zakład to osobny INSERT. Jeśli zapisywać każdy od razu do tabeli MergeTree, ClickHouse będzie tworzył tysiące małych fragmentów (parts), co spowalnia tła scalania i obniża wydajność.

Buffer ENGINE rozwiązuje ten problem: gromadzi wstawienia w buforze w pamięci i zapisuje je do docelowej tabeli w dużych paczkach po spełnieniu warunków (liczba wierszy, objętość lub czas).

Google AdInline article slot

Składnia z parametrami:

CREATE TABLE bets_buffer AS bets   -- kopiuje strukturę tabeli bets
ENGINE = Buffer(
    'default',        -- nazwa bazy danych tabeli docelowej
    'bets',           -- nazwa tabeli docelowej (tam będą zrzucane dane)
    16,               -- liczba równoległych wątków do flush
    10,               -- minimalne opóźnienie w sekundach (min_time)
    100,              -- maksymalne opóźnienie w sekundach (max_time)
    10000,            -- minimalna liczba wierszy do flush
    1000000,          -- maksymalna liczba wierszy do flush
    10000000,         -- minimalny rozmiar w bajtach do flush
    100000000         -- maksymalny rozmiar w bajtach do flush
);

Parametry flush (zrzutu bufora na dysk):

Parametr Wartość Co oznacza
min_time 10 s Nie zrzucaj wcześniej niż po 10 sekundach
max_time 100 s Zrzuć nie później niż po 100 sekundach
min_rows 10 000 Jeśli zebrało się 10k wierszy — można zrzucić
max_rows 1 000 000 Jeśli zebrał się 1 mln wierszy — pilnie zrzuć
min_bytes 10 MB Jeśli zebrało się 10 MB — można zrzucić
max_bytes 100 MB Jeśli zebrało się 100 MB — pilnie zrzuć

Jak to działa w rzeczywistości:

  1. Wstawiasz do bets_buffer (szybko, po prostu zapis w pamięci).
  2. ClickHouse czeka, aż zbierze się wystarczająco danych (np. 100k wierszy lub minęło 30 sekund).
  3. Następnie asynchronicznie (w tle) zrzuca paczkę do głównej tabeli bets (MergeTree).
  4. W rezultacie do bets trafiają duże fragmenty (100k wierszy), co przyspiesza tła scalania.

Dlaczego nie wstawiać od razu do MergeTree? Każdy INSERT do MergeTree tworzy mini-fragment (part). Jeśli robisz 10 000 INSERT na sekundę, po minucie będzie 600 000 fragmentów. Tło merge nie nadąża ich sklejać. ClickHouse zacznie narzekać na Too many parts, wstawianie zwolni.

Pułapka: Po restarcie ClickHouse bufor jest tracony. Dane, które jeszcze nie zostały zrzucone do bets, znikną. Dlatego używaj Buffer ENGINE tylko jeśli utrata kilku sekund danych nie jest krytyczna (np. dla analityki, a nie dla bilansów).

3. Null ENGINE — czarna dziura dla danych

Null ENGINE po prostu pochłania dane. Nie są nigdzie zapisywane, przechowywane ani indeksowane. Jest jednak sztuczka: jeśli tabela z Null ENGINE ma widoki zmaterializowane (Materialized Views), to widoki otrzymają dane i je przetworzą.

Wzorzec: Kafka → Null + Materialized View → MergeTree

To klasyczna architektura dla obciążonych wstawień z Kafka.

-- Krok 1: Tabela-odbiornik (czarna dziura)
CREATE TABLE bets_null
(
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = Null;   -- nic nie przechowuje

-- Krok 2: Tabela docelowa (tam faktycznie zapisujemy)
CREATE TABLE bets
(
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = MergeTree()
ORDER BY (created_at, user_id);

-- Krok 3: Widok zmaterializowany (most)
CREATE MATERIALIZED VIEW bets_mv TO bets AS
SELECT * FROM bets_null;   -- wszelkie dane, które trafią do bets_null, trafiają do bets

Co teraz się dzieje:

-- Klient (lub konsument Kafka) wstawia do bets_null
INSERT INTO bets_null VALUES (123, 100.00, now());   -- natychmiast

-- Dane przechodzą przez Materialized View i są zapisywane w bets
-- Same nie są przechowywane w bets_null

Po co to robić?

  • bets_null to bardzo lekka tabela, nie tworzy plików na dysku.
  • Wszyscy subskrybenci (Materialized Views) otrzymują dane jednocześnie.
  • Można podpiąć kilka Materialized Views do jednej tabeli Null: jeden dla surowych danych w MergeTree, drugi dla agregatów w AggregatingMergeTree, trzeci dla deduplikacji w ReplacingMergeTree.

Analogia: Null ENGINE to jak skrzynka pocztowa z dziurawym dnem. Listy wpadają, ale nie zatrzymują się. Za to wszyscy twoi sekretarze (Materialized Views) zdążą je przeczytać i przepisać do swoich teczek.

4. Log/TinyLog/StripeLog — proste silniki dla małych danych

To rodzina silników dla małych tabel (do 1–2 mln wierszy), gdzie nie potrzeba wysokiej wydajności i indeksów.

Silnik Cechy Kiedy używać
TinyLog Jeden plik na kolumnę Bardzo małe tabele (<100k wierszy), staging
Log Każda kolumna w osobnym pliku, jest znacznik (marker) do równoległego odczytu Tabele do 1 mln wierszy, potrzebny szybki odczyt
StripeLog Wszystkie kolumny w jednym pliku (kompaktowo) Oszczędność miejsca, rzadki odczyt

Przykład dla słownika lig (200 rekordów):

-- Słownik lig piłkarskich (zmienia się raz w miesiącu)
CREATE TABLE leagues_ref
(
    league_id   UInt32,
    name        String,
    country     String,
    updated_at  Date
)
ENGINE = TinyLog();   -- maksymalnie prosto, brak ORDER BY

Dlaczego nie MergeTree? MergeTree tworzy indeksy, partycje, kompresję — to przesada dla 200 wierszy. TinyLog zajmuje mniej miejsca i jest prostszy w utrzymaniu.

Pułapka: Te silniki nie obsługują ALTER DELETE i ALTER UPDATE. Jeśli trzeba zmienić dane — trzeba odtworzyć tabelę.

5. URL ENGINE — tabela jako endpoint HTTP

URL ENGINE pozwala czytać dane bezpośrednio ze źródła HTTP (API), a nawet wstawiać je przez PUT.

CREATE TABLE currency_rates_url
(
    base   String,
    rate   Decimal(10,4),
    date   Date
)
ENGINE = URL('https://api.exchangerate.com/latest?base=USD', CSV)
SETTINGS 
    method = 'GET',
    format = 'CSV',
    headers = 'Authorization: Bearer token123';

Użycie:

-- Czytamy aktualne kursy bezpośrednio z API
SELECT * FROM currency_rates_url;

Rzeczywisty scenariusz: Małe zadanie analityczne, gdzie nie chce się wdrażać ETL. Na przykład raz na godzinę czytasz kursy walut z darmowego API, łączysz z zakładami i przeliczasz kwoty.

Pułapki:

  • Brak indeksów, każde zapytanie to pełne skanowanie źródła.
  • Jeśli API zwróci błąd, zapytanie upadnie.
  • Nie nadaje się do obciążonych zapytań (zakłada się, że dane są buforowane wewnątrz ClickHouse, a nie czytane z API za każdym razem).

6. File ENGINE — tabela jako plik na dysku

Pozwala czytać i zapisywać pliki w lokalnym systemie plików serwera ClickHouse. Obsługuje formaty CSV, TSV, JSONEachRow, Parquet.

-- Tabela, która czyta plik CSV
CREATE TABLE imported_players
(
    user_id UInt64,
    username String
)
ENGINE = File(CSV, '/var/lib/clickhouse/user_files/players.csv');

Kiedy używać:

  • Ładowanie danych z plików (administrator wrzucił CSV z nowymi użytkownikami).
  • Eksport wyników zapytania do pliku przez INSERT INTO ... SELECT.

Pułapka: ClickHouse musi mieć dostęp do folderu (zwykle jest to /var/lib/clickhouse/user_files/ dla bezpieczeństwa).

7. S3 ENGINE — bezpośrednie zapytania do S3

Czyta dane bezpośrednio z bucketa Amazon S3 (lub MinIO, Yandex Object Storage). Nie kopiuje danych do ClickHouse.

CREATE TABLE logs_s3
(
    timestamp DateTime,
    message String
)
ENGINE = S3(
    'https://mybucket.s3.amazonaws.com/logs/*.parquet',
    'AWS_ACCESS_KEY', 'AWS_SECRET_KEY',
    'Parquet'
);

Kiedy używać:

  • Masz terabajty logów w S3 i chcesz wykonywać rzadkie zapytania analityczne bez kopiowania do ClickHouse.
  • Zimne dane (S3 jest tańsze niż dyski ClickHouse).

Pułapka: Każde zapytanie ściąga dane z S3, co może być wolne i drogie (za ruch wychodzący). Nadaje się tylko do rzadkich zapytań.

8. PostgreSQL ENGINE — live-dane z PostgreSQL

PostgreSQL ENGINE pozwala czytać i zapisywać do tabel PostgreSQL tak, jakby były tabelami ClickHouse.

CREATE TABLE pg_players
(
    user_id UInt64,
    balance Decimal(18,2)
)
ENGINE = PostgreSQL(
    'postgres-host:5432',   -- host i port
    'betting',               -- baza danych
    'players',               -- tabela w PostgreSQL
    'clickhouse_user',       -- użytkownik
    'password'               -- hasło
);

Użycie:

-- Czytamy aktualne salda z PostgreSQL
SELECT * FROM pg_players WHERE user_id = 123;

-- Można nawet robić JOIN z tabelami ClickHouse
SELECT b.user_id, b.amount, p.balance
FROM bets b
JOIN pg_players p ON b.user_id = p.user_id;

Kiedy używać:

  • Migrujesz z PostgreSQL do ClickHouse stopniowo, a niektóre dane wciąż żyją w starej bazie.
  • Potrzebne są live-dane, które są aktualizowane przez zewnętrzną aplikację, i nie chcesz robić ETL.

Pułapki:

  • Każde zapytanie idzie do PostgreSQL, co może być wolne dla dużych wolumenów.
  • ClickHouse nie może zbudować efektywnego planu wykonania z udziałem takiej tabeli (No pushdown).

9. Wzorzec architektoniczny dla betting: Kafka → Buffer → MergeTree

Teraz zbierzmy wszystko razem. Wyobraź sobie, że od każdego zdarzenia zakładu otrzymujesz 10 000+ wiadomości na sekundę z Kafka. Trzeba je zapisać w ClickHouse z minimalnym opóźnieniem i bez tworzenia tysięcy małych fragmentów.

Gotowa architektura:

-- 1. Tabela-odbiornik (Null) dla Kafka
CREATE TABLE bets_kafka
(
    user_id     UInt64,
    event_id    UInt64,
    amount      Decimal(18,2),
    bet_time    DateTime
)
ENGINE = Null;

-- 2. Docelowa tabela MergeTree
CREATE TABLE bets
(
    user_id     UInt64,
    event_id    UInt64,
    amount      Decimal(18,2),
    bet_time    DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(bet_time)
ORDER BY (bet_time, user_id);

-- 3. Tabela buforowa do wygładzania wstawień
CREATE TABLE bets_buffer AS bets
ENGINE = Buffer('default', 'bets', 16, 5, 60, 10000, 1000000, 10000000, 100000000);

-- 4. Materialized View: Kafka → Null → Buffer (przez widok)
CREATE MATERIALIZED VIEW bets_kafka_mv TO bets_buffer AS
SELECT * FROM bets_kafka;

-- 5. Kolejny widok dla agregatów w czasie rzeczywistym (opcjonalnie)
CREATE MATERIALIZED VIEW bets_stats_mv TO bets_hourly_agg AS
SELECT 
    toStartOfHour(bet_time) AS hour,
    countState() AS bet_count,
    sumState(amount) AS total_amount
FROM bets_kafka
GROUP BY hour;

Przepływ danych:

  1. Kafka Connect wstawia wiadomości do bets_kafka (Null ENGINE).
  2. bets_kafka_mv (Materialized View) przekierowuje dane do bets_buffer.
  3. bets_buffer gromadzi w pamięci paczki (np. 100k wierszy lub 60 sekund).
  4. Bufor zrzuca do bets (MergeTree) dużymi fragmentami.
  5. Równolegle drugi Materialized View buduje agregaty godzinowe dla dashboardów.

Dlaczego to optymalne:

  • Kafka pisze do Null (natychmiast, bez narzutów).
  • Buffer nie pozwala na rozmnożenie tysięcy małych fragmentów.
  • MergeTree otrzymuje duże fragmenty, scalanie działa efektywnie.
  • Agregaty są budowane w czasie rzeczywistym przez drugi widok.

Co się stanie, jeśli pisać bezpośrednio do MergeTree z Kafka: Przy 10k events/sec w ciągu minuty powstanie 600k fragmentów. ClickHouse padnie z błędem Too many parts. Buffer ENGINE ratuje przed tym.

Kiedy jaki silnik wybrać — ściągawka

Zadanie Silnik Dlaczego
Stałe przechowywanie, analityka MergeTree (lub *MergeTree) Podstawa ClickHouse, indeksy, kompresja
Buforowanie wysokich obciążeń Buffer Skleja małe INSERT w duże fragmenty
Dane tymczasowe (sesja, staging) Memory Maksymalna szybkość, dane niekrytyczne
Konsument Kafka bez przechowywania Null + MV Dane tylko dla widoków
Mały statyczny słownik TinyLog / Log Prostota, mniej metadanych
Podłączenie do PostgreSQL PostgreSQL ENGINE Żywe dane bez ETL
Rzadkie zapytania do S3 S3 ENGINE Tanie zimne przechowywanie
Import z plików File ENGINE Jednorazowe kopiowanie

Co dalej

Poznałeś specjalne silniki, które rozwiązują zadania, niedostępne dla zwykłego MergeTree. Teraz wiesz:

  • Memory do cache i staging,
  • Buffer do ochrony przed zbyt częstymi INSERT,
  • Null do "rozgałęziania" danych na kilka strumieni przez MV,
  • URL / File / S3 / PostgreSQL do danych zewnętrznych.

Następne tematy do pogłębienia:

  • Jak skonfigurować Kafka Engine — wbudowany łącznik do Kafka bez osobnego łącznika.
  • Zaawansowane Materialized Views — łańcuchy widoków dla złożonych ETL.
  • Tabele rozproszone — jak shardować dane między serwerami.

Podsumowanie: Nie wszystkie zadania w ClickHouse rozwiązuje MergeTree. Czasem potrzebny jest Buffer, żeby nie zabić serwera wstawieniami, czasem Null + MV, żeby rozesłać dane do różnych agregatów, a czasem Memory do tymczasowego haszowania. Główna zasada: najpierw zaprojektuj przepływ danych, potem wybierz silnik, a nie odwrotnie.


Poprzedni:
Następny: Materializowane widoki w ClickHouse: potęga przetwarzania przyrostowego

— Editorial Team

Advertisement 728x90

Czytaj dalej