ClickHouse 클라이언트: 콘솔과 HTTP API와 친해진 방법 (도박 프로젝트 사례)
닫힌 문을 처음 두드리며
ClickHouse를 설치한 후, 기쁜 마음으로 clickhouse-client를 입력했더니 오류가 발생했습니다: Code: 210. DB::NetException: Connection refused (localhost:9000). 알고 보니 서버가 127.0.0.1에서만 수신 대기 중이었고, 저는 다른 머신에서 연결하려고 했던 것입니다. 한 시간 동안 구글링하고 config.xml을 수정하고 재시작한 끝에, 클라이언트에 --host와 --port 플래그가 있다는 것을 알게 되었습니다.
ClickHouse는 두 가지 통신 방식을 제공합니다: 네이티브 클라이언트(사람과 스크립트용)와 HTTP API(그 외 모든 것용). 저는 매일 둘 다 사용합니다. 아래는 실제로 필요한 모든 것과 제가 겪은 함정들입니다.
연결 방법: 간단한 것부터 제대로 된 것까지
방법 1. 기본 (로컬호스트 전용)
clickhouse-client
이 방법은 서버와 같은 머신에 있고 포트 9000을 변경하지 않은 경우에만 작동합니다. 프로덕션에서는 아무도 이렇게 하지 않습니다.
방법 2. 전문가용: 원격 접속 플래그
clickhouse-client \
--host analytics.prod.company.com \
--port 9000 \
--user analyst \
--password 'StrongPass123' \
--database betting
프로덕션에서 배운 점: bash 히스토리가 활성화되어 있으면 명령줄에 비밀번호를 절대 입력하지 마세요. 대신 설정 파일을 사용하세요.
방법 3. 제대로: 설정 파일
~/.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>
환경 변수를 통한 비밀번호:
export CLICKHOUSE_PASSWORD="StrongPass123"
clickhouse-client
이 방법이 더 안전한 이유: 비밀번호가 ps aux나 히스토리에 남지 않습니다. 프로덕션에서 한 개발자가 clickhouse-client --password secret을 실행했고, 한 시간 안에 모든 사람이 오케스트레이터 로그에서 비밀번호를 볼 수 있었던 사례가 있었습니다.
방법 4. HTTP 연결 (CI/CD 대안)
자동화를 위해 HTTP를 자주 사용합니다:
curl -u analyst:StrongPass123 \
"http://clickhouse.prod.internal:8123/?query=SELECT+1"
대화형 vs 배치: 언제 무엇을 사용할까
대화형 모드 (사람용)
clickhouse-client
장점: 자동 완성(Tab 키), 명령 히스토리, 여러 줄 쿼리. 단점: 스크립트에 적합하지 않음.
:) SELECT user_id, sum(amount) FROM bets GROUP BY user_id LIMIT 5;
제 꿀팁: 대화형 모드에서는 \l (데이터베이스 목록), \d (테이블 목록), \c betting (데이터베이스 전환) 같은 단축키가 작동합니다. 모두가 아는 것은 아니지만, 시간을 크게 절약해 줍니다.
배치 모드 (스크립트 및 크론용)
# 단일 명령
clickhouse-client --query "SELECT count() FROM betting.bets"
# 파일에서
clickhouse-client --queries-file /path/to/analytics.sql
# 여러 줄 (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
제가 당한 점: 배치 모드에서는 항상 쿼리 끝에 세미콜론을 붙이세요. 없으면 명령이 실행되지 않지만 오류도 표시되지 않고 그냥 멈춥니다. 크론 작업 디버깅에 한 시간을 낭비했습니다.
HTTP API: curl이 최고의 친구
HTTP 인터페이스는 포트 8123에서 실행됩니다. 마이크로서비스, 대시보드, 모든 언어의 스크립트에 완벽합니다.
GET 요청: 간단하고 빠름
# 가장 간단한 쿼리
curl "http://localhost:8123/?query=SELECT+version()"
# 인증 포함
curl -u user:pass "http://localhost:8123/?query=SELECT+count()+FROM+betting.bets"
# 데이터베이스 파라미터 포함
curl "http://localhost:8123/?database=betting&query=SELECT+count()+FROM+bets"
POST 요청: 대용량 쿼리 및 데이터 삽입용
# POST로 긴 쿼리 (URL 길이 제한 없음)
curl -X POST "http://localhost:8123/" \
-d "SELECT user_id, sum(amount) FROM betting.bets GROUP BY user_id"
# POST로 데이터 삽입
curl -X POST "http://localhost:8123/?query=INSERT+INTO+betting.bets+FORMAT+CSV" \
--data-binary @bets_data.csv
응답 형식: 작업에 맞게 선택
ClickHouse는 다양한 형식으로 데이터를 반환할 수 있습니다. 모두 시도해 봤습니다. 실제로 필요한 것은 다음과 같습니다:
# Pretty — 사람용 (읽기 쉽지만 포맷 문자가 많음)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=Pretty"
# JSON — API용 (어디서나 파싱 가능)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=JSON"
# JSONEachRow — 줄 단위 처리용 (메모리 효율적)
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=JSONEachRow"
# CSV — Excel/Google Sheets로 내보내기용
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=CSV"
# TabSeparated — 다른 도구(grep, awk)로 파이프용
curl "http://localhost:8123/?query=SELECT+user_id,amount+FROM+bets+LIMIT+3&default_format=TSV"
실제 예: 집계 데이터를 텔레그램 봇으로 보냅니다. JSONEachRow를 사용하고, Python에서 response.json() 한 줄로 파싱한 후 메시지로 포맷합니다.
베팅 플랫폼용 데이터베이스 생성
CREATE DATABASE IF NOT EXISTS betting;
그리고 즉시 전환:
clickhouse-client --database betting
또는 클라이언트 내부에서:
USE betting;
첫 번째 테이블: 실제 프로젝트의 베팅 스키마
베팅 분석을 위한 프로덕션 프로젝트에서 테이블은 다음과 같습니다:
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);
이렇게 한 이유:
LowCardinalityfor sport — 축구, 농구, 테니스. 수천 번 반복되며 사전으로 압축됩니다.Enum8for outcome — 세 가지 값만 있으므로 문자열 대신 1바이트를 사용합니다.DateTime64(3)— 실시간 베팅 분석에는 밀리초가 중요합니다.
초보자의 흔한 실수: ENGINE = MergeTree()를 지정하지 않는 것입니다. 없으면 ClickHouse가 TinyLog 엔진(테스트 전용)으로 테이블을 생성하며, 파티셔닝이 불가능하고 복제를 지원하지 않습니다. 프로덕션에서 이런 테이블에 1천만 행을 삽입하면 죽습니다.
테스트 데이터 삽입
단일 레코드
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');
여러 레코드 (배치 삽입)
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');
numbers()로 테스트 데이터 생성
부하 테스트를 위해 종종 즉시 백만 개의 레코드를 생성합니다:
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);
중요 참고: 이 삽입은 괜찮은 서버에서 5-10초가 걸립니다. ClickHouse는 이러한 대량 작업에 최적화되어 있지만, 약한 VM에서는 1분이 걸릴 수 있습니다.
베팅 맥락에서의 기본 SELECT 쿼리
WHERE — 필터링
-- 지난 1시간 동안 특정 사용자의 베팅
SELECT *
FROM betting.bets
WHERE user_id = 1001
AND created_at >= now() - INTERVAL 1 HOUR;
-- 배당률 2.0 이상인 당첨 베팅
SELECT user_id, amount, odds, amount * odds AS payout
FROM betting.bets
WHERE outcome = 'win' AND odds > 2.0;
ORDER BY — 정렬
-- 오늘 가장 큰 베팅
SELECT user_id, amount, created_at
FROM betting.bets
WHERE created_at >= today()
ORDER BY amount DESC
LIMIT 10;
-- 사용자의 최근 5개 베팅
SELECT created_at, sport, amount, odds, outcome
FROM betting.bets
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 5;
GROUP BY — 분석
-- 주간 스포츠별 지급액
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;
조건 결합
-- 하루에 10회 이상 베팅한 사용자
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;
실용 사례: 의심스러운 패턴 찾기
다음은 사기 탐지 시스템의 실제 쿼리입니다:
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;
이 쿼리는 봇을 찾습니다: 시간당 100회 이상 거의 동일한 금액으로 베팅하는 사용자. PostgreSQL에서 5천만 레코드로는 절대 끝나지 않을 것입니다. ClickHouse는 0.6초 만에 답을 반환합니다.
데이터 내보내기: 비즈니스에 제공해야 할 때
# 마케팅 부서용 CSV로 내보내기
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
# 다른 서비스 API용 JSON으로 내보내기
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
일반적인 오류와 해결 방법
오류: Code: 102. DB::NetException: Connection refused
원인: 호스트나 포트가 잘못되었거나 서버가 외부 연결을 수신하지 않음.
해결 방법: netstat -tulpn | grep clickhouse 확인. 0.0.0.0:9000이 없으면 config.xml 수정:
<listen_host>0.0.0.0</listen_host>
오류: Code: 81. DB::Exception: Database betting doesn't exist
원인: 데이터베이스를 생성하지 않았거나 --database를 지정하지 않음.
해결 방법: CREATE DATABASE IF NOT EXISTS betting; 또는 --database betting으로 연결.
오류: Code: 62. DB::Exception: Syntax error: failed at position 1
원인: 배치 모드에서 세미콜론을 빼먹음.
해결 방법: --query 사용 시 항상 쿼리 끝에 ;를 붙이세요.
다음은?
이제 ClickHouse에 어떤 방식으로든 연결하고, 테이블을 생성하고, 데이터를 삽입하고, 쿼리를 실행하는 방법을 알게 되었습니다. 다음 글에서는 고급 분석: 윈도우 함수, 배열, 집계, 구체화된 뷰에 대해 다루겠습니다.
모든 예제는 ClickHouse 24.8에서 테스트되었습니다. 작동하지 않으면 먼저 버전을 확인하세요: SELECT version(); 이 방법이 수백 번 저를 구했습니다.
← 이전 글: Docker에서 ClickHouse: 걱정을 접고 2분 만에 분석 시작하기
→ 다음 글: ClickHouse: 베팅 분석을 위한 완벽한 데이터 타입 가이드 (내가 당한 경험담)
— Editorial Team
아직 댓글이 없습니다.