ClickHouse — 열 지향 분석 DB 를 속까지 · SummingMergeTree 와 AggregatingMergeTree · 실습
요약 표를 나눠 넣고, 합과 상태로 원본과 같은 답을 낸다
목표
원본 이벤트에서 (site, day) 요약을 SummingMergeTree 와 AggregatingMergeTree 에 나눠 넣고, 병합 전에도 원본과 똑같은 답을 내는 조회를 씁니다. 합으로 줄일 수 없는 값은 상태로 담아야 한다는 것과 그 상태를 더 큰 단위로 다시 합치는 법을 확인합니다.
왜 중요한가
요약 표는 대시보드를 싸게 만들지만, 병합이 끝나기 전에는 같은 키가 여러 행으로 흩어져 있습니다. 그 상태에서 SELECT * 로 읽거나, 서로 다른 사용자 수를 숫자로 저장해 더하면 틀린 답이 조용히 나옵니다. 이 실습은 병합을 멈춰 그 틀린 상태를 고정하고, 채점기가 여러분의 쿼리 결과를 원본 agg.hits 를 직접 집계한 값과 칸마다 대조합니다. 몇 번에 나눠 넣었는지는 system.part_log 기록으로 확인합니다.
단계
1. 데이터베이스 agg 와 표 agg.hits 를 만드세요 — 열 ts DateTime, site LowCardinality(String), user_id UInt64, dur_ms UInt32, is_bot UInt8(이 순서), 엔진 MergeTree, ORDER BY (site, ts).
2. /opt/lab/fixtures/aggregating/hits.sql 을 한 번 실행해 100만 행을 넣으세요.
3. agg.daily (site LowCardinality(String), day Date, hits UInt64, dur_total UInt64) 를 ENGINE = SummingMergeTree ORDER BY (site, day) 로 만들고 SYSTEM STOP MERGES agg.daily 로 병합을 멈춘 뒤, agg.hits 를 시간대별(toHour(ts) < 8, 8 이상 16 미만, 16 이상)로 INSERT 세 번에 나눠 site, toDate(ts), count(), sum(dur_ms) 를 넣으세요.
4. 병합 전인 지금도 원본과 같은 (site, day)별 조회수·체류 합을 내는 /root/ch/aggregating/q_daily.sql 을 만드세요(agg.daily 만 읽고, 열은 site, day, hits, dur_total 순서). 그리고 agg.daily 의 지금 행 수와 서로 다른 (site, day) 수를 /root/ch/aggregating/raw.json 에 raw_rows·keys 로 적으세요.
5. SYSTEM START MERGES agg.daily 뒤 OPTIMIZE TABLE agg.daily FINAL 로 합쳐, 키마다 한 행이 되게 하세요.
6. agg.daily_state (site LowCardinality(String), day Date, users AggregateFunction(uniqExact, UInt64), avg_dur AggregateFunction(avg, UInt32), human_hits AggregateFunction(countIf, UInt8)) 를 ENGINE = AggregatingMergeTree ORDER BY (site, day) 로 만들고 병합을 멈춘 뒤, 3단계와 같은 시간대 세 묶음으로 uniqExactState(user_id)·avgState(dur_ms)·countIfState(is_bot = 0) 을 넣으세요.
7. agg.daily_state 만 읽어 (site, day)별 users·avg_dur·human_hits 를 내는 /root/ch/aggregating/q_state.sql 을 만드세요(열은 site, day, users, avg_dur, human_hits 순서). 그리고 병합 전 각 행의 finalizeAggregation(users) 를 그냥 더한 값과 q_state.sql 의 users 를 모두 더한 값을 /root/ch/aggregating/naive.json 에 naive_users_sum·true_users_sum 으로 적으세요.
8. agg.day_total (day Date, users AggregateFunction(uniqExact, UInt64)) 를 ENGINE = AggregatingMergeTree ORDER BY day 로 만들어 agg.daily_state 의 상태를 uniqExactMergeState(users) 로 날짜별로 다시 합쳐 넣고, agg.day_total 만 읽어 날짜별 (사이트 구분 없는) 서로 다른 사용자 수를 내는 /root/ch/aggregating/q_day.sql 을 만드세요(열은 day, users).
참고
- 서버는 파드가 뜰 때 이미 떠 있습니다.
clickhouse-client만 치면 붙습니다. 멈췄다면ch-up(서버를 다시 띄우면 STOP MERGES 가 풀립니다). - INSERT 기록은
system.part_log(event_type = 'NewPart')에 남습니다. 채점기는 표의 UUID 로 거릅니다. 다시 하려면TRUNCATE TABLE뒤 세 번 넣으면 됩니다. - 상태 열은 사람이 읽는 값이 아닙니다. 확인할 때는
finalizeAggregation(열)이나-Merge함수로 끝내서 봅니다. - 흔한 실수: 한 INSERT 로 원본 전체를 넣는 것 — 같은 키가 넣는 순간 합쳐져 "병합 전" 을 볼 수 없습니다. 조회에서
sum(uniqExactMerge(...))처럼 끝낸 값을 다시 더하는 것 — 겹치는 사람을 여러 번 셉니다. - 공식 문서: [SummingMergeTree](https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/summingmergetree) · [AggregatingMergeTree](https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/aggregatingmergetree) · [Aggregate Function Combinators](https://clickhouse.com/docs/reference/functions/aggregate-functions/combinators) · [system.part_log](https://clickhouse.com/docs/reference/system-tables/part_log)
8단계
- 원본 이벤트 표
- 원본 100만 행
- SummingMergeTree 에 세 번 나눠 넣는다
- 병합 전에도 맞는 조회
- 병합하면 키마다 한 행
- 합으로 줄일 수 없는 값은 상태로
- 상태는 합친 뒤에 끝낸다
- 상태를 더 큰 단위로 다시 합친다