집계 함수와 컴비네이터 (-If, -Array, -State, -Merge, uniq, quantile)
ClickHouse 집계 함수의 핵심인 컴비네이터와 근사 함수를 정리합니다. 한 번 읽으면 CASE WHEN 더미와 서브쿼리 없이 집계 쿼리를 절반 길이로 줄일 수 있고, uniq와 quantile의 오차를 알고 고를 수 있습니다.
왜 컴비네이터부터 배우나
ClickHouse 집계 함수의 특징은 함수 이름 뒤에 접미사(컴비네이터) 를 붙여 동작을 바꾸는 점입니다. countIf, sumArray, uniqState처럼요. 다른 DB에서 SUM(CASE WHEN ... THEN 1 ELSE 0 END)로 쓰던 것이 countIf(...) 하나로 끝나고, 배열 컬럼을 풀지 않고 집계할 수 있고, 중간 상태를 저장해 뒤에 합칠 수 있습니다.
SELECT
toDate(event_time) AS d,
count() AS pv,
countIf(status >= 500) AS err,
uniq(user_id) AS uv,
uniqIf(user_id, is_new = 1) AS new_uv,
quantile(0.95)(duration_ms) AS p95,
argMax(url, duration_ms) AS slowest_url
FROM web_logs
WHERE event_time >= today() - 7
GROUP BY d
ORDER BY d;자주 쓰는 기본 집계 함수
| 함수 | 하는 일 | 메모 |
|---|---|---|
count(), count(col) | 행 수, NULL 아닌 값 수 | count(*)도 됩니다 |
sum, avg, min, max | 표준과 같음 | sum은 넘침을 막기 위해 더 큰 타입으로 계산 |
any, anyLast | 그룹의 임의 값 하나 | 그룹에서 "대표값 하나만" 필요할 때 GROUP BY에 넣지 않고 꺼냅니다 |
argMax(a, b), argMin(a, b) | b가 최대(최소)인 행의 a | 최신 상태 뽑기의 표준 패턴 |
groupArray, groupUniqArray | 값을 배열로 모음 | 순서 보장은 없음. 정렬이 필요하면 groupArraySorted(n) 또는 arraySort |
topK(n), topKWeighted | 빈도 상위 n개 근사 | 정확한 순위는 GROUP BY + ORDER BY |
sumMap, maxMap | Map 또는 키 배열과 값 배열을 키별로 합산 | 카운터 맵 집계에 유용 |
avgWeighted(x, w) | 가중 평균 | |
groupBitAnd, groupBitmap | 비트 연산, 비트맵 | 사용자 세그먼트 교집합 등 |
varPop, stddevPop, corr | 통계 | 표본 버전은 varSamp, stddevSamp |
sparkbar(n), histogram(n) | 작은 시각화, 히스토그램 | 탐색용 |
팁 "각 사용자의 마지막 로그인 시각과 그때의 IP"는
argMax(ip, login_at)과max(login_at)을 같은 GROUP BY에서 꺼내면 끝입니다. 윈도우 함수나 자기 조인이 필요하지 않습니다.
컴비네이터 한눈에
| 접미사 | 뜻 | 예 |
|---|---|---|
-If | 조건을 만족하는 행만 | sumIf(amount, status = 'paid') |
-Array | 배열 컬럼의 원소를 펼쳐서 집계 | sumArray(items_price), uniqArray(tags) |
-Map | Map의 값을 키별로 집계 | sumMap(counters) |
-State | 중간 상태를 돌려줌 (AggregateFunction 타입) | uniqState(user_id) |
-Merge | 중간 상태들을 합쳐 최종값 | uniqMerge(uv_state) |
-MergeState | 상태들을 합쳐 다시 상태로 | 롤업 테이블을 더 큰 단위로 다시 집계 |
-SimpleState | SimpleAggregateFunction용 상태 | maxSimpleState(x) |
-ForEach | 배열의 같은 위치끼리 집계 | sumForEach([1,2],[3,4])는 [4,6] |
-Distinct | 중복 값을 하나로 | sumDistinct(x) |
-OrDefault, -OrNull | 빈 그룹이면 기본값 또는 NULL | maxOrNull(x), avgOrDefault(x) |
-Resample | 구간별로 나눠 배열로 집계 | countResample(0, 100, 10)(age) |
-ArgMin, -ArgMax | 다른 컬럼이 최소, 최대인 행만 대상으로 | sumArgMax(amount, version) |
컴비네이터는 겹쳐 쓸 수 있습니다. uniqArrayIf(tags, status = 200)은 조건에 맞는 행의 배열을 펼쳐 고유 수를 셉니다. countIfState, sumMapMerge처럼 상태 컴비네이터와도 섞입니다. 접미사 순서는 안쪽 동작이 먼저 오도록, 보통 -Array, -If, 그다음 -State 순으로 붙입니다.
-- 조건별 집계를 CASE 없이
SELECT
device,
countIf(status < 400) AS ok,
countIf(status BETWEEN 400 AND 499) AS client_err,
countIf(status >= 500) AS server_err,
avgIf(duration_ms, status < 400) AS ok_avg_ms
FROM web_logs
GROUP BY device;
-- 배열 컬럼을 ARRAY JOIN 없이
SELECT uniqArray(tags) AS distinct_tags, sumArray(item_prices) AS gmv
FROM orders
WHERE order_date = today();근사 함수의 오차를 알고 고르기
uniq 가족
| 함수 | 방식 | 오차 | 메모리 | 쓸 때 |
|---|---|---|---|---|
uniq | 적응형 샘플링 해시 | 보통 1% 안쪽 | 작음 | 대시보드 기본값 |
uniqCombined, uniqCombined64 | HyperLogLog++ 계열 | 약 0.5% 안쪽 (정밀도 인자로 조정) | 작고 안정적 | 상태를 저장하는 롤업 테이블 |
uniqHLL12 | HyperLogLog 고정 | 수 % | 2.5KB 고정 | 메모리가 아주 빡빡할 때 |
uniqTheta | Theta sketch | 교집합, 차집합 연산 가능 | 중간 | 세그먼트 겹침 계산 |
uniqExact | 해시 집합 | 0 | 고유 값 수에 비례 | 정산, 보고서 |
count(DISTINCT x)는 설정 count_distinct_implementation이 가리키는 함수(기본값 uniqExact)로 바뀝니다. 대시보드에서 count(DISTINCT user_id)를 쓰면 메모리를 많이 쓰니 uniq로 바꿀 것인지 먼저 정합니다.
quantile 가족
| 함수 | 방식 | 특징 |
|---|---|---|
quantile, median | 저수지 샘플링 (기본 8192개) | 빠르고 메모리가 일정. 데이터가 많으면 결과가 조금씩 흔들림 |
quantileExact | 전부 메모리에 올려 정렬 | 정확, 메모리 큼 |
quantileTDigest | t-digest | 양 끝 분위수(p99.9)에 강함, 상태 작음 |
quantileTiming | 밀리초 지연 특화 | 정확도와 속도 균형, 30초 넘는 값은 뭉개짐 |
quantileBFloat16 | 16비트 근사 | 아주 빠름, 오차 큼 |
quantileDD | DDSketch | 상대 오차 보장 |
여러 분위수를 한 번에 구할 때는 quantiles(0.5, 0.9, 0.99)(x)처럼 복수형을 쓰면 데이터를 한 번만 읽습니다. 함수마다 quantilesTDigest, quantilesTiming이 있습니다.
SELECT
service,
quantilesTiming(0.5, 0.95, 0.99)(duration_ms) AS p,
p[1] AS p50, p[2] AS p95, p[3] AS p99
FROM requests
WHERE event_time >= now() - INTERVAL 1 HOUR
GROUP BY service
ORDER BY p99 DESC;참고 분위수 함수에 넘기는 값이 정렬 키의 일부라면
optimize_aggregation_in_order가 켜졌을 때 메모리를 덜 씁니다. 그래도 근사 함수 선택이 메모리에 가장 큰 영향을 줍니다.
-State와 -Merge: 미리 집계해 두기
-State는 집계의 중간 결과를 AggregateFunction(함수, 타입) 값으로 돌려줍니다. 이 값은 사람이 읽을 수 없지만 합칠 수 있습니다. 시간별 롤업 테이블에 상태를 저장해 두면, 일별이나 월별 고유 사용자 수를 다시 원본을 읽지 않고 -Merge로 구합니다. 머티리얼라이즈드 뷰와 AggregatingMergeTree의 핵심입니다.
CREATE TABLE daily_uv
(
d Date,
country LowCardinality(String),
uv AggregateFunction(uniqCombined, UInt64),
pv SimpleAggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (d, country);
CREATE MATERIALIZED VIEW daily_uv_mv TO daily_uv AS
SELECT toDate(event_time) AS d, country,
uniqCombinedState(user_id) AS uv, count() AS pv
FROM web_logs
GROUP BY d, country;
-- 조회: 상태를 합쳐 최종값으로
SELECT d, uniqCombinedMerge(uv) AS uv, sum(pv) AS pv
FROM daily_uv
WHERE d >= today() - 30
GROUP BY d ORDER BY d;uniqCombinedMerge처럼 같은 함수 이름으로 합쳐야 합니다.uniqState를uniqCombinedMerge로 합칠 수 없습니다.sum,max,min,any,groupBitOr처럼 상태가 값 자체인 함수는SimpleAggregateFunction을 쓰면 저장과 조회가 단순해집니다.-MergeState는 상태를 합쳐 다시 상태로 돌려주므로 일별 롤업에서 월별 롤업을 만들 때 씁니다.- 상태 컬럼은
finalizeAggregation(state)로 바로 최종값을 볼 수 있습니다.
전체 설계는 머티리얼라이즈드 뷰와 MergeTree 가족에서 이어집니다.
GROUP BY 메모리와 설정
큰 GROUP BY에서 메모리 초과가 나면 다음 순서로 손봅니다.
- 근사 함수로 바꾸기 (
uniqExact를uniq로,quantileExact를quantileTDigest로). - 키 컬럼을
LowCardinality(String)으로, 고카디널리티 문자열 키는 해시나 숫자 ID로. max_bytes_before_external_group_by를 두어 디스크로 흘리기. 보통max_memory_usage의 절반 정도로 잡습니다.- 정렬 키 접두어로 집계한다면
optimize_aggregation_in_order = 1. max_rows_to_group_by와group_by_overflow_mode = 'any'로 상위 그룹만 보고 멈추기 (탐색용).
SELECT url, uniq(user_id) AS uv
FROM web_logs
GROUP BY url
ORDER BY uv DESC
LIMIT 100
SETTINGS max_bytes_before_external_group_by = 10000000000,
max_memory_usage = 20000000000;메모리 오류의 원인 분류는 메모리 초과 트러블슈팅에 있습니다.
WITH TOTALS, ROLLUP, CUBE, GROUPING SETS
SELECT country, device, count() AS c
FROM web_logs
WHERE event_date = today()
GROUP BY country, device WITH ROLLUP
ORDER BY country, device;WITH TOTALS는 결과 뒤에 전체 합계 행을 따로 붙입니다.totals_mode로 HAVING에 걸린 행을 합계에 넣을지 정합니다.ROLLUP은 (country, device), (country), () 순서로 상위 합계를 만듭니다.GROUPING SETS로 필요한 조합만 고르고,grouping(country)로 어느 수준의 행인지 구분합니다.
집계 쿼리를 쓸 때 체크리스트
- 조건 집계는
-If, 배열은-Array, 두 개 이상이면 겹쳐 쓰기. - 최신 상태는
argMax, 대표값은any. - 고유 수는 목적에 맞는
uniq*. 정산은uniqExact. - 분위수는
quantiles*(...)로 한 번에. 지연시간은quantileTiming또는quantileTDigest. - 반복되는 집계는
-State로 롤업 테이블에 저장하고-Merge로 읽기. - 메모리 초과는 근사 함수, LowCardinality, 외부 집계 순으로.