ClickHouse client: jak zaprzyjaźniłem się z konsolą i HTTP API w projekcie gamblingowym
Moje pierwsze pukanie do zamkniętych drzwi
Pamiętam, jak po instalacji ClickHouse z radością wpisałem clickhouse-client i dostałem błąd: Code: 210. DB::NetException: Connection refused (localhost:9000). Okazało się, że serwer słucha tylko na 127.0.0.1, a ja próbowałem połączyć się z innej maszyny. Godzina googlowania, edycja config.xml, restart — i dopiero wtedy dowiedziałem się, że klient ma flagi --host i --port.
ClickHouse daje dwa sposoby komunikacji: natywny klient (dla ludzi i skryptów) oraz HTTP API (dla wszystkiego innego). Używam obu każdego dnia. Poniżej — wszystko, co naprawdę potrzebne, plus grabie, na które nadepnąłem.
Sposoby połączenia: od prostego do właściwego
Sposób 1. Naiwny (tylko dla localhost)
clickhouse-client
To działa tylko wtedy, gdy jesteś na tej samej maszynie co serwer i nie zmieniałeś portu 9000. W produkcji nikt tak nie robi.
Sposób 2. Profesjonalny: flagi dla zdalnego dostępu
clickhouse-client \
--host analytics.prod.company.com \
--port 9000 \
--user analyst \
--password 'StrongPass123' \
--database betting
Co wyniosłem z produkcji: nigdy nie przekazuj hasła w wierszu poleceń, jeśli włączona jest historia bash. Zamiast tego użyj pliku konfiguracyjnego.
Sposób 3. Właściwy: plik konfiguracyjny
Tworzymy ~/.clickhouse-client/config.xml:
<config>
<host>clickhouse.prod.internal</host>
<port>9000</port>
<user>analyst</user>
<password>${CLICKHOUSE_PASSWORD}</password>
<database>betting</database>
<history_file>/home/user/.clickhouse-client-history</history_file>
</config>
Hasło przez zmienną środowiskową:
export CLICKHOUSE_PASSWORD="StrongPass123"
clickhouse-client
Dlaczego to bezpieczniejsze: hasło nie trafi do ps aux ani historii. W produkcji mieliśmy przypadek, gdy programista uruchomił clickhouse-client --password secret, a po godzinie wszyscy widzieli hasło w logach orkiestratora.
Sposób 4. Połączenie przez HTTP (alternatywa dla CI/CD)
Do automatyzacji często używam HTTP:
curl -u analyst:StrongPass123 \
"http://clickhouse.prod.internal:8123/?query=SELECT+1"
Tryb interaktywny vs batch: kiedy czego używać
Tryb interaktywny (dla ludzi)
clickhouse-client
Zalety: autouzupełnianie (naciśnij Tab), historia poleceń, zapytania wieloliniowe. Wady: nie do skryptów.
:) SELECT user_id, sum(amount) FROM bets GROUP BY user_id LIMIT 5;
Mój lifehack: w trybie interaktywnym działają skróty \l (lista baz), \d (lista tabel), \c betting (zmiana bazy). Nie wszyscy wiedzą, ale oszczędza mnóstwo czasu.
Tryb batch (dla skryptów i cron)
# Jedno polecenie
clickhouse-client --query "SELECT count() FROM betting.bets"
# Z pliku
clickhouse-client --queries-file /path/to/analytics.sql
# Wieloliniowe przez heredoc
clickhouse-client <<SQL
SELECT
toDate(created_at) AS day,
count() AS bets
FROM betting.bets
WHERE created_at >= today() - 7
GROUP BY day
ORDER BY day;
SQL
Na czym się sparzyłem: w trybie batch kończ zapytanie średnikiem. Bez niego polecenie się nie wykona, ale też nie pokaże błędu — po prostu zawisnie. Straciliśmy godzinę na debugowanie zadań cron.
HTTP API: curl — twój najlepszy przyjaciel
Interfejs HTTP działa na porcie 8123. Jest idealny dla mikrousług, dashboardów i skryptów w dowolnym języku.
Zapytania GET: prosto i szybko
# Najprostsze zapytanie
curl "http://localhost:8123/?query=SELECT+version()"
# Z autoryzacją
curl -u user:pass "http://localhost:8123/?query=SELECT+count()+FROM+betting.bets"
# Z parametrem database
curl "http://localhost:8123/?database=betting&query=SELECT+count()+FROM+bets"
Zapytania POST: dla dużych zapytań i wstawiania danych
# Długie zapytanie przez POST (bez limitu długości URL)
curl -X POST "http://localhost:8123/" \
-d "SELECT user_id, sum(amount) FROM betting.bets GROUP BY user_id"
# Wstawianie danych przez POST
curl -X POST "http://localhost:8123/?query=INSERT+INTO+betting.bets+FORMAT+CSV" \
--data-binary @bets_data.csv
Formaty odpowiedzi: wybierz pod zadanie
ClickHouse potrafi zwracać dane w wielu formatach. Przetestowałem wszystkie — oto co naprawdę jest potrzebne:
# Pretty — dla człowieka (czytelny, ale dużo znaków kontrolnych)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=Pretty"
# JSON — dla API (parsowalny wszędzie)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=JSON"
# JSONEachRow — do przetwarzania linia po linii (oszczędny pamięciowo)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=JSONEachRow"
# CSV — do eksportu do Excel/Google Sheets
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=CSV"
# TabSeparated — do przekazywania do innych narzędzi (grep, awk)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=TSV"
Przykład z życia: Wysyłamy agregaty do bota telegramowego. Używamy JSONEachRow, parsujemy w Pythonie jedną linią response.json() i formatujemy w wiadomość.
Tworzymy bazę danych dla platformy bukmacherskiej
CREATE DATABASE IF NOT EXISTS betting;
I od razu przełączamy się na nią:
clickhouse-client --database betting
Lub już wewnątrz klienta:
USE betting;
Pierwsza tabela: schemat zakładów z rzeczywistego projektu
W moim projekcie produkcyjnym do analityki zakładów tabela wygląda tak:
CREATE TABLE betting.bets
(
user_id UInt64,
created_at DateTime64(3),
amount Decimal(18, 2),
odds Float64,
sport LowCardinality(String),
outcome Enum8('win' = 1, 'loss' = 2, 'void' = 3),
event_id UInt64,
bet_type String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (created_at, user_id);
Dlaczego tak:
LowCardinalitydla sport — piłka nożna, koszykówka, tenis. Powtarzają się tysiące razy, kompresują się do słownika.Enum8dla outcome — tylko trzy wartości, zajmuje 1 bajt zamiast stringa.DateTime64(3)— milisekundy są ważne dla analityki zakładów na żywo.
Częsty błąd początkujących: zapominają podać ENGINE = MergeTree(). Bez tego ClickHouse utworzy tabelę z silnikiem TinyLog (tylko do testów), której nie można partycjonować i która nie wspiera replikacji. W produkcji przy wstawianiu 10 mln wierszy taka tabela umrze.
Wstawiamy dane testowe
Jeden rekord
INSERT INTO betting.bets (user_id, created_at, amount, odds, sport, outcome, event_id, bet_type)
VALUES (1001, now(), 50.00, 2.1, 'football', 'win', 50001, 'single');
Wiele rekordów (wstawianie wsadowe)
INSERT INTO betting.bets VALUES
(1002, now() - INTERVAL 1 HOUR, 100.00, 1.8, 'basketball', 'loss', 50002, 'single'),
(1003, now() - INTERVAL 2 HOUR, 200.00, 3.0, 'tennis', 'win', 50003, 'express'),
(1001, now() - INTERVAL 30 MINUTE, 75.00, 2.5, 'football', 'void', 50001, 'single');
Generowanie danych testowych przez numbers()
Do testów obciążeniowych często generuję milion rekordów na bieżąco:
INSERT INTO betting.bets
SELECT
number % 10000 AS user_id,
now() - INTERVAL (number % 86400) SECOND,
(number % 1000) / 10 + 10,
1.5 + (number % 200) / 100,
arrayElement(['football', 'basketball', 'tennis', 'hockey'], (number % 4) + 1),
CAST((number % 3) + 1 AS Enum8('win' = 1, 'loss' = 2, 'void' = 3)),
number,
'single'
FROM numbers(1000000);
Ważna uwaga: to wstawianie zajmie 5-10 sekund na normalnym serwerze. ClickHouse jest zoptymalizowany do takich operacji wsadowych, ale na słabej wirtualce może ładować się minutę.
Podstawowe zapytania SELECT w kontekście zakładów
WHERE — filtrowanie
-- Zakłady konkretnego użytkownika z ostatniej godziny
SELECT *
FROM betting.bets
WHERE user_id = 1001
AND created_at >= now() - INTERVAL 1 HOUR;
-- Wygrane zakłady z kursem większym niż 2.0
SELECT user_id, amount, odds, amount * odds AS payout
FROM betting.bets
WHERE outcome = 'win' AND odds > 2.0;
ORDER BY — sortowanie
-- Największe zakłady dzisiaj
SELECT user_id, amount, created_at
FROM betting.bets
WHERE created_at >= today()
ORDER BY amount DESC
LIMIT 10;
-- Ostatnie 5 zakładów użytkownika
SELECT created_at, sport, amount, odds, outcome
FROM betting.bets
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 5;
GROUP BY — analityka
-- Wypłaty według dyscyplin sportowych w ciągu tygodnia
SELECT
sport,
count() AS total_bets,
sum(amount) AS total_staked,
sumIf(amount * odds, outcome = 'win') AS total_payout,
round(total_payout / total_staked, 4) AS roi
FROM betting.bets
WHERE created_at >= today() - 7
GROUP BY sport
ORDER BY total_bets DESC;
Kombinacje warunków
-- Użytkownicy, którzy postawili więcej niż 10 zakładów dzisiaj
SELECT
user_id,
count() AS bets_count,
sum(amount) AS total_amount
FROM betting.bets
WHERE created_at >= today()
GROUP BY user_id
HAVING bets_count > 10
ORDER BY total_amount DESC;
Praktyczny przypadek: znajdowanie podejrzanych wzorców
Oto prawdziwe zapytanie z naszego systemu detekcji fraudów:
WITH hourly_bets AS (
SELECT
user_id,
toStartOfHour(created_at) AS hour,
count() AS bets_per_hour,
avg(amount) AS avg_bet
FROM betting.bets
WHERE created_at >= now() - INTERVAL 3 HOUR
GROUP BY user_id, hour
)
SELECT
user_id,
max(bets_per_hour) AS max_rate,
avg(avg_bet) AS typical_bet,
stddevPop(avg_bet) AS bet_variance
FROM hourly_bets
GROUP BY user_id
HAVING max_rate > 100 AND bet_variance < 0.5;
To zapytanie znajduje boty: tych, którzy stawiają częściej niż 100 razy na godzinę i z prawie identyczną kwotą. W PostgreSQL przy 50 mln rekordów nigdy by się nie wykonało. ClickHouse zwraca odpowiedź w 0,6 sekundy.
Eksport danych: gdy trzeba oddać biznesowi
# Eksport do CSV dla działu marketingu
clickhouse-client --query "
SELECT user_id, sum(amount) AS total_bet, count() AS bet_count
FROM betting.bets
WHERE created_at >= '2024-01-01'
GROUP BY user_id
ORDER BY total_bet DESC
LIMIT 1000
" --format CSV > top_users.csv
# Eksport do JSON dla API innego serwisu
curl "http://localhost:8123/?query=SELECT+user_id,sum(amount)+FROM+betting.bets+GROUP+BY+user_id+LIMIT+10&default_format=JSON" \
-o top_users.json
Typowe błędy i ich rozwiązania
Błąd: Code: 102. DB::NetException: Connection refused
Przyczyna: Zły host lub port, albo serwer nie słucha połączeń zewnętrznych.
Rozwiązanie: Sprawdź netstat -tulpn | grep clickhouse. Jeśli nie ma 0.0.0.0:9000, edytuj config.xml:
<listen_host>0.0.0.0</listen_host>
Błąd: Code: 81. DB::Exception: Database betting doesn't exist
Przyczyna: Nie utworzyłeś bazy lub nie podałeś --database.
Rozwiązanie: CREATE DATABASE IF NOT EXISTS betting; lub połącz się z flagą --database betting.
Błąd: Code: 62. DB::Exception: Syntax error: failed at position 1
Przyczyna: Zapomniałeś średnika w trybie batch.
Rozwiązanie: Zawsze stawiaj ; na końcu zapytania, jeśli używasz --query.
Co dalej?
Teraz umiesz łączyć się z ClickHouse na każdy sposób, tworzyć tabele, wstawiać dane i robić zapytania. W następnym artykule omówimy zaawansowaną analitykę: funkcje okienne, tablice, agregacje i widoki zmaterializowane.
Wszystkie przykłady przetestowane na ClickHouse 24.8. Jeśli coś nie działa — najpierw sprawdź wersję: SELECT version(); To ratowało mnie setki razy.
← Poprzedni: ClickHouse w Docker: jak przestałem się bać i uruchomiłem analitykę w 2 minuty
→ Następny: ClickHouse: pełny przewodnik po typach danych dla analityki zakładów (na czym się sparzyłem)
— Editorial Team
Brak komentarzy.