ClickHouse — 열 지향 분석 DB 를 속까지 · 구체화 뷰는 INSERT 트리거 · 실습
구체화 뷰가 본 것과 못 본 것
목표
원천 표에 구체화 뷰를 걸어 INSERT 가 대상 표로 흐르는 것을 확인하고, 뷰가 보지 못하는 세 가지 — 뷰 이전의 행, 병합 전의 중복 키, JOIN 오른쪽 표의 변화 — 를 숫자로 재현한다.
왜 중요한가
ClickHouse 의 구체화 뷰는 "저장된 쿼리" 가 아니라 INSERT 트리거입니다. 그래서 되채우기를 빠뜨리면 과거가 비고, 두 번 되채우면 두 배가 되고, 대상 표를 더하지 않고 읽으면 병합 시점에 따라 숫자가 흔들리고, 차원 표 조인은 늦게 온 차원을 영영 놓칩니다. 이 실습의 채점기는 여러분이 적은 숫자를 믿지 않습니다 — 대상 표를 원천 표(또는 픽스처 생성 식)로 다시 계산한 값과 칸마다 대조하고, 저장한 SELECT 는 읽기 전용으로 다시 돌려 결과와 읽은 행 수를 잽니다.
단계
1. 데이터베이스 mv 와 표 mv.orders 를 만드세요 — 열 order_id UInt64, ts DateTime, shop LowCardinality(String), product_id UInt32, user_id UInt32, qty UInt32, price UInt32 (이 순서), 엔진 MergeTree, ORDER BY (shop, ts). 그리고 /opt/lab/fixtures/mv/orders_1.sql(9월 1–15일, 20만 건)을 한 번 넣으세요.
2. 대상 표 mv.daily_sales (day Date, shop LowCardinality(String), orders UInt64, revenue UInt64) 를 SummingMergeTree, ORDER BY (shop, day) 로 만들고, 구체화 뷰 mv.daily_sales_mv 를 TO mv.daily_sales 로 만드세요 — toDate(ts) AS day, shop, count() AS orders, sum(qty * price) AS revenue 를 GROUP BY day, shop.
3. /opt/lab/fixtures/mv/orders_2.sql(9월 16–30일, 20만 건)을 넣은 직후, mv.orders 의 행 수를 source_orders, mv.daily_sales 의 sum(orders) 를 view_orders 로 /root/ch/mv/gap.json 에 적으세요.
4. 뷰가 보지 못한 1차분을 mv.daily_sales 에 되채워, 대상 표가 원천 전체를 (shop, day) 별로 정확히 담게 하세요. 2차분을 또 넣으면 안 됩니다.
5. mv.daily_sales 에서 shop-3 의 날짜별 주문 수·매출을 구하는 /root/ch/mv/q_daily.sql 을 쓰세요 — 열 day, orders, revenue, 날짜순. 병합 전에도 맞도록 더해서 읽어야 합니다.
6. 대상 표 mv.shop_stats (shop LowCardinality(String), day Date, orders AggregateFunction(count), buyers AggregateFunction(uniqExact, UInt32)) 를 AggregatingMergeTree, ORDER BY (shop, day) 로, 뷰 mv.shop_stats_mv 를 TO mv.shop_stats 로(countState()·uniqExactState(user_id)) 만들고 이미 있는 원천을 한 번 되채우세요. 그리고 상점별 한 달 고유 구매자를 mv.shop_stats 에서 구하는 /root/ch/mv/q_buyers.sql 을 쓰세요 — 열 shop, buyers, 상점순.
7. mv.orders 와 같은 열에 status LowCardinality(String) 을 끝에 더한 mv.raw 를 ENGINE = Null 로 만들고, status = 'paid' 인 행만 mv.orders 로 보내는 뷰 mv.raw_mv 를 만든 뒤 /opt/lab/fixtures/mv/raw.sql 을 한 번 넣으세요.
8. mv.products (product_id UInt32, category LowCardinality(String))(MergeTree, ORDER BY product_id)와 mv.cat_sales (category LowCardinality(String), orders UInt64)(SummingMergeTree, ORDER BY category), 그리고 mv.orders 를 mv.products 와 INNER JOIN 해 범주별 count() 를 보내는 뷰 mv.cat_sales_mv 를 만드세요. 그다음 products.sql → late.sql → products_new.sql (모두 /opt/lab/fixtures/mv/) 순서로 넣고, 늦은 주문 수(order_id >= 2000000)를 late_orders, mv.cat_sales 의 sum(orders) 를 joined_orders, 그 차이를 lost_orders 로 /root/ch/mv/join.json 에 적으세요.
참고
- 서버는 이미 떠 있습니다(
clickhouse-client). 멈췄다면ch-up. - 읽은 행을 잴 때는
--use_query_condition_cache 0을 주세요. 같은 조건의 쿼리를 두 번째 돌리면 조건 캐시가 그래뉼을 건너뛰어 숫자가 달라질 수 있습니다. 채점기도 캐시를 끄고 잽니다. - 픽스처는 주문 번호로 구간이 나뉩니다 — 1차분 0부터, 2차분 200000부터, 원시 1000000부터, 늦은 주문 2000000부터.
- 흔한 실수: 되채우기에 2차분까지 넣어 9월 16–30일이 두 배가 되는 것, JOIN 뷰를 LEFT JOIN 으로 만들어 빈 범주가 생기는 것, 픽스처를 두 번 넣는 것.
- 망가뜨렸다면 대상 표는
TRUNCATE TABLE뒤 원천 전체를 한 번 다시 넣으면 됩니다(뷰는 그대로 두고). - 공식 문서: [Incremental materialized view](https://clickhouse.com/docs/concepts/features/materialized-views/incremental-materialized-view) · [CREATE VIEW](https://clickhouse.com/docs/reference/statements/create/view) · [Use materialized views](https://clickhouse.com/docs/concepts/best-practices/use-materialized-views) · [AggregatingMergeTree](https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/aggregatingmergetree) · [Refreshable materialized view](https://clickhouse.com/docs/concepts/features/materialized-views/refreshable-materialized-view)
8단계
- 원천 표를 만들고 1차분을 넣는다
- 대상 표와 구체화 뷰를 만든다
- 뷰가 본 것과 원천을 비교한다
- 빠진 구간만 되채운다
- 병합 전에도 맞게 대상 표를 읽는다
- 더할 수 없는 값은 상태로 담는다
- Null 표에서 시작하는 연쇄 뷰
- JOIN 뷰는 오른쪽 표의 변화를 모른다