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ć:
- 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:
- Tabela Memory nie obsługuje scalania (merge) — jeśli robisz wiele UPDATE (przez wstawianie z anulowaniem), pamięć będzie puchnąć. Użyj
TRUNCATEdo 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).
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:
- Wstawiasz do
bets_buffer(szybko, po prostu zapis w pamięci). - ClickHouse czeka, aż zbierze się wystarczająco danych (np. 100k wierszy lub minęło 30 sekund).
- Następnie asynchronicznie (w tle) zrzuca paczkę do głównej tabeli
bets(MergeTree). - W rezultacie do
betstrafiają 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_nullto 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:
- Kafka Connect wstawia wiadomości do
bets_kafka(Null ENGINE). bets_kafka_mv(Materialized View) przekierowuje dane dobets_buffer.bets_buffergromadzi w pamięci paczki (np. 100k wierszy lub 60 sekund).- Bufor zrzuca do
bets(MergeTree) dużymi fragmentami. - 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: Słowniki w ClickHouse: szybki lookup bez JOIN
→ Następny: Materializowane widoki w ClickHouse: potęga przetwarzania przyrostowego
— Editorial Team
Brak komentarzy.