Real-Time Market AnalyticsClickHouse Workshops

04 시장 쿼리 실행하기

26.5M건의 틱을 대상으로 각각 SQL 한 페이지를 대체하는 일곱 개의 쿼리: 근사 카운트, topK, OHLC 캔들스틱, 백분위수, -If 컴비네이터, argMax, 그리고 sort key 피날레.

이것들이 바로 ClickHouse의 "SQL 한 페이지를 대체하는 함수들"입니다. 각 블록을 붙여넣고 실행한 뒤, 초록색 박스에서 무엇이 나와야 하는지 확인하세요. 모두 방금 로드한 forex 테이블을 대상으로 실행됩니다.

4.1 근사값 대 정확값 — 핵심 트릭

26.5M건의 틱에 서로 다른 호가 타임스탬프는 몇 개나 있을까요? 세 함수가 같은 질문에 서로 다른 트레이드오프로 답합니다. 한 번에 하나씩 실행하면서 쿼리 통계의 실행 시간을 보세요 — 따로 실행하는 것이 이 절의 핵심입니다.

-- Approximate count of distinct timestamps (HyperLogLog)
SELECT uniq(datetime) AS distinct_ts FROM forex;
-- Exact count — precise, but scans every value
SELECT uniqExact(datetime) AS distinct_ts FROM forex;
-- Adaptive + tunable accuracy vs memory
SELECT uniqCombined(datetime) AS distinct_ts FROM forex;

이렇게 보여야 합니다

세 함수 모두 약 2,460만 개의 고유 타임스탬프를 반환합니다. 하지만 uniq는 약 0.2초에 돌아오고, uniqExact는 정확한 숫자를 얻기 위해 약 3초(약 17배 느림)가 걸리며, uniqCombined는 1초도 훨씬 안 되는 시간에 오차 약 0.4% 안으로 들어옵니다. 수십억 행 규모에서 이 격차는 즉시 결과가 나오는 것과 커피 한 잔 마시고 오는 것의 차이입니다.

4.2 함수 하나로 Top-N

가장 활발하게 호가된 통화 쌍 — GROUP BY / ORDER BY / LIMIT이 필요 없습니다:

-- One row: an array of the 8 most actively quoted pairs (by tick count)
SELECT topK(8)(pair) AS most_active FROM forex;

이렇게 보여야 합니다

8개 통화 쌍의 배열이 담긴 셀 하나가 나오고, 호가가 많은 순서입니다(금, XAU/USD가 1위). topK는 한 번의 패스로 테이블 전체를 그 순위 목록으로 압축합니다 — GROUP BY … ORDER BY count() DESC LIMIT 8을 대신하는 셈입니다.

4.3 한 번의 패스로 시계열 — OHLC 캔들스틱

금의 일별 시가 / 고가 / 저가 / 종가 — 시장 분석의 기본 쿼리입니다:

-- One row per day: gold (XAU/USD) open, high, low, close — a candlestick chart
SELECT
    toDate(datetime)      AS day,
    argMin(bid, datetime) AS open,    -- bid at the day's first tick
    max(bid)              AS high,
    min(bid)              AS low,
    argMax(bid, datetime) AS close    -- bid at the day's last tick
FROM forex
WHERE base = 'XAU' AND quote = 'USD'
GROUP BY day
ORDER BY day;

이렇게 보여야 합니다

2020년 1월의 하루당 한 행. argMin(bid, datetime)은 그날 첫 틱의 bid(시가)이고 argMax는 마지막 틱의 bid(종가)입니다 — 윈도 함수도, 셀프 조인도 없습니다. 금이 한 달 동안 약 1,520에서 약 1,610까지 오르는 것을 보세요.

4.4 윈도 함수 곡예 없이 백분위수

bid/ask 스프레드는 유동성 지표입니다 — 그리고 평균은 꼬리를 감추므로 백분위수를 사용하세요:

-- One row per pair: median and 99th-percentile bid/ask spread (tighter = more liquid)
SELECT
    pair,
    round(quantile(0.5)(ask - bid), 6)  AS median_spread,
    round(quantile(0.99)(ask - bid), 6) AS p99_spread
FROM forex
GROUP BY pair
ORDER BY median_spread ASC;

이렇게 보여야 합니다

통화 쌍당 한 행, 스프레드가 가장 좁은 것부터. EUR/USD가 가장 유동성이 높게 나옵니다 (약 0.00002, 1핍 이하). 금이 가장 넓습니다. quantile은 한 번의 패스로 근사 계산됩니다 — 컬럼 전체를 정렬하지 않습니다.

4.5 컴비네이터 — 한 번의 스캔, 두 개의 답

활발한 거래 시간대와 한산한 시간대의 평균 스프레드를 한 번의 스캔으로 나란히 비교합니다:

-- One row per pair: avg spread during the active window vs quiet hours
SELECT
    pair,
    count()                                                           AS ticks,
    round(avgIf(ask - bid, toHour(datetime) BETWEEN 7 AND 20), 6)     AS spread_active,
    round(avgIf(ask - bid, toHour(datetime) NOT BETWEEN 7 AND 20), 6) AS spread_quiet
FROM forex
GROUP BY pair
ORDER BY ticks DESC;

이렇게 보여야 합니다

활발한 시간대와 한산한 시간대의 스프레드가 같은 결과에 함께 나옵니다. -If 컴비네이터는 어떤 집계 함수에든 조건을 붙여줍니다: avgIf(x, cond)는 cond가 참인 곳의 x만 평균합니다 — 두 번 스캔하는 쿼리도, CASE WHEN도 필요 없습니다. 거의 모든 집계 함수가 이를 지원합니다 (countIf, sumIf, quantileIf, …).

4.6 최신 값 — argMax

모든 통화 쌍의 가장 최근 호가를 한 번의 패스로:

-- One row per pair: the latest quoted bid and the timestamp it was seen
SELECT
    pair,
    argMax(bid, datetime) AS last_bid,
    max(datetime)         AS as_of
FROM forex
GROUP BY pair
ORDER BY pair;

이렇게 보여야 합니다

통화 쌍별 최신 bid. argMax(bid, datetime)은 datetime이 가장 큰 행의 bid를 반환합니다 — 윈도 함수나 셀프 조인 없이 처리하는, 일상적인 "마지막 가격 / 최신 상태" 쿼리입니다.

4.7 피날레 — sort key가 있을 때와 없을 때

쿼리 모양은 같습니다 — 조건에 맞는 틱을 세는 것 — 하지만 한 필터는 sort key를 타고 다른 하나는 그렇지 않습니다. 먼저 두 카운트를 실행하세요:

-- Optimized: base+quote ARE the leading sort key — the index skips to that pair
SELECT count() FROM forex WHERE base = 'XAU' AND quote = 'USD';

-- Non-optimized: bid is NOT in the sort key — no index, scans all 26.5M ticks.
-- (cache off so the full scan shows on every run)
SELECT count() FROM forex WHERE bid > 1.5
SETTINGS use_query_condition_cache = 0;

차이를 알아보기 어렵나요?

두 카운트 모두 몇 밀리초 만에 돌아오므로 시간만 보면 차이가 거의 없습니다 — count()는 어느 쪽이든 빠릅니다. 진짜 차이는 각각이 얼마나 많은 데이터를 건드려야 하는가입니다. 이를 실제로 보려면 EXPLAIN indexes = 1로 ClickHouse의 실행 계획을 확인하세요. 읽게 될 그래뉼(약 8,192행 블록) 수를 알려줍니다.

-- Optimized: the primary index skips straight to the XAU/USD rows
EXPLAIN indexes = 1
SELECT count() FROM forex WHERE base = 'XAU' AND quote = 'USD';

-- Non-optimized: bid isn't in the sort key, so nothing can be skipped
EXPLAIN indexes = 1
SELECT count() FROM forex WHERE bid > 1.5;

이렇게 보여야 합니다

최적화된 계획에서는 Indexes → PrimaryKey 섹션에 선택된 그래뉼이 일부만 표시됩니다 — 약 492 / 3,234(약 16K 행). 최적화되지 않은 계획에서는 적용할 인덱스가 없으므로 3,234 / 3,234 그래뉼, 즉 모든 행을 읽습니다. 같은 쿼리 모양인데 작업량은 완전히 다릅니다. 교훈: 가장 자주 필터링하는 컬럼을 ORDER BY 앞쪽에 두세요.

이 쿼리는 클립보드에 남겨두세요

최적화되지 않은 그 WHERE bid > 1.5 쿼리를 다음 모듈에서 AI에게 넘기게 됩니다.

이 페이지의 내용

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

KO