LabHub
배우기 러닝패스 코스

ClickHouse — 列指向分析 DB を中身から

要約表を分けて挿入し、合計と状態で元データと同じ答えを出す

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

원본 이벤트에서 (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.jsonraw_rows·keys 로 적으세요.
  5. SYSTEM START MERGES agg.dailyOPTIMIZE 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.jsonnaive_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).

참고

원본 이벤트 표

데이터베이스 agg 와 표 agg.hits 를 만드세요. 열은 ts DateTime, site LowCardinality(String), user_id UInt64, dur_ms UInt32, is_bot UInt8 순서, 엔진은 MergeTree, 정렬 키는 ORDER BY (site, ts) 입니다.

요약 표는 늘 원본에서 만들어집니다. 원본을 MergeTree 로 그대로 두면 요약을 잘못 만들어도 다시 만들 수 있습니다 — 문서가 SummingMergeTree 를 MergeTree 와 함께 쓰라고 권하는 이유입니다.

원본 100만 행

/opt/lab/fixtures/aggregating/hits.sql한 번 실행해 agg.hits 에 1,000,000행을 넣으세요.

clickhouse-client 에 --queries-file 로 넘기면 됩니다. 사이트 5개 × 2026년 9월 30일이라 (site, day) 조합은 150개입니다. 두 번 넣었다면 TRUNCATE 뒤 다시 넣으세요.

SummingMergeTree 에 세 번 나눠 넣는다

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) 를 (site, day) 로 묶어 넣으세요.

하루치 요약이 세 번에 나눠 도착하는 상황입니다. 각 INSERT 는 그 시간대만 GROUP BY 해서 넣으므로, 병합 전에는 같은 (site, day) 가 세 행으로 있습니다. 병합을 멈추는 것은 넣기 전에 해야 합니다.

병합 전에도 맞는 조회

agg.daily 만 읽어 (site, day)별 조회수·체류 합을 원본과 똑같이 내는 /root/ch/aggregating/q_daily.sql 을 만드세요. 열은 site, day, hits, dur_total 순서입니다. 그리고 agg.daily 의 지금 행 수와 서로 다른 (site, day) 수를 /root/ch/aggregating/raw.jsonraw_rows·keys 로 적으세요.

병합이 끝났는지 알 수 없으니 조회에서 한 번 더 묶어 더합니다. SELECT * 로 읽으면 지금은 키마다 세 행이 나옵니다. 원본 agg.hits 를 읽으면 안 됩니다 — 요약 표만으로 답을 내는 것이 목적입니다.

병합하면 키마다 한 행

SYSTEM START MERGES agg.dailyOPTIMIZE TABLE agg.daily FINAL 로 합쳐, agg.daily 가 파트 하나·키마다 한 행이 되게 하세요.

병합이 같은 키의 숫자 열을 더해 한 행으로 접습니다. 합친 뒤 SELECT * 가 원본 집계와 같아지는지 보세요. 운영에서는 병합 시점을 모르므로 4단계 같은 조회가 여전히 필요합니다.

합으로 줄일 수 없는 값은 상태로

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) 로 만들고 SYSTEM STOP MERGES agg.daily_state 뒤, 3단계와 같은 시간대 세 묶음으로 uniqExactState(user_id)·avgState(dur_ms)·countIfState(is_bot = 0) 을 (site, day) 로 묶어 넣으세요.

열 타입 AggregateFunction(함수, 인자 타입) 은 그 함수의 중간 상태를 담는다는 뜻이고, 넣을 때는 같은 함수에 -State 를 붙여 만듭니다. -If 조합자는 조건을 마지막 인자로 받습니다. 병합 전이라 세 묶음의 상태가 따로 있어야 합니다.

상태는 합친 뒤에 끝낸다

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.jsonnaive_users_sum·true_users_sum 으로 적으세요.

-Merge 를 붙인 함수가 같은 키의 상태들을 합친 뒤 결과를 냅니다. finalizeAggregation 은 상태 하나를 그 자리에서 끝내므로, 행마다 끝낸 숫자를 더하면 여러 시간대에 온 사람을 여러 번 셉니다. 두 합이 얼마나 다른지 보세요.

상태를 더 큰 단위로 다시 합친다

agg.day_total (day Date, users AggregateFunction(uniqExact, UInt64))ENGINE = AggregatingMergeTree ORDER BY day 로 만들어 agg.daily_state 의 users 상태를 uniqExactMergeState(users) 로 날짜별로 합쳐 넣고, agg.day_total 만 읽어 날짜별(사이트 구분 없는) 서로 다른 사용자 수를 내는 /root/ch/aggregating/q_day.sql 을 만드세요(열은 day, users).

-MergeState 는 상태들을 합쳐 결과가 아니라 다시 상태를 돌려주므로, 그 결과를 또 다른 AggregatingMergeTree 에 넣을 수 있습니다. 원본을 다시 읽지 않고 사이트별 상태에서 만드세요. 사이트별 사용자 수(숫자)를 더하면 같은 사람을 여러 번 셉니다.