머티어리얼라이즈드 뷰 롤업
머티어리얼라이즈드 뷰 롤업 (Materialized view rollup)
고부하 이벤트 테이블에서 사전 집계된 롤업을 머티어리얼라이즈드 뷰로 유지하는 방법을 살펴봐요.
출처: 문서
본문
이 튜토리얼은 머티어리얼라이즈드 뷰를 사용해 고부하 이벤트 테이블에서 사전 집계된 롤업을 유지하는 방법을 보여줘요. 원시 테이블, 롤업 테이블, 그리고 롤업에 자동으로 쓰는 머티어리얼라이즈드 뷰라는 세 가지 객체를 만들게 돼요.
이 패턴을 언제 사용할까
다음 경우에 이 패턴을 사용해요:
- append-only 이벤트 스트림(클릭, 페이지뷰, IoT, 로그)이 있어요.
- 대부분의 쿼리가 시간 범위에 걸친 집계(분/시간/일 단위)예요.
- 모든 원시 행을 다시 스캔하지 않고 일관된 서브초 읽기를 원해요.
1. 원시 이벤트 테이블 만들기
CREATE TABLE events_raw
(
event_time DateTime,
user_id UInt64,
country LowCardinality(String),
event_type LowCardinality(String),
value Float64
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id)
TTL event_time + INTERVAL 90 DAY DELETE
참고
PARTITION BY toYYYYMM(event_time)는 파티션을 작게 유지하고 드롭하기 쉽게 해줘요.ORDER BY (event_time, user_id)는 시간 범위 쿼리 + 보조 필터를 지원해요.LowCardinality(String)는 범주형 차원의 메모리를 절약해요.TTL은 90일 후 원시 데이터를 정리해요(보존 요구 사항에 맞게 조정).
2. 롤업(집계) 테이블 설계하기
시간별(hourly) 세분성으로 사전 집계할게요. 가장 일반적인 분석 창에 맞는 세분성을 선택해요.
CREATE TABLE events_rollup_1h
(
bucket_start DateTime, -- start of the hour
country LowCardinality(String),
event_type LowCardinality(String),
users_uniq AggregateFunction(uniqExact, UInt64),
value_sum AggregateFunction(sum, Float64),
value_avg AggregateFunction(avg, Float64),
events_count AggregateFunction(count)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(bucket_start)
ORDER BY (bucket_start, country, event_type)
부분 집계를 간결하게 나타내며 나중에 병합되거나 확정될 수 있는 집계 상태(예: AggregateFunction(sum, ...))를 저장해요.
3. 롤업을 채우는 머티어리얼라이즈드 뷰 만들기
이 머티어리얼라이즈드 뷰는 events_raw에 대한 삽입 시 자동으로 발화하고 집계 상태를 롤업에 써요.
CREATE MATERIALIZED VIEW mv_events_rollup_1h
TO events_rollup_1h
AS
SELECT
toStartOfHour(event_time) AS bucket_start,
country,
event_type,
uniqExactState(user_id) AS users_uniq,
sumState(value) AS value_sum,
avgState(value) AS value_avg,
countState() AS events_count
FROM events_raw
GROUP BY bucket_start, country, event_type;
4. 샘플 데이터 삽입하기
샘플 데이터를 삽입해요:
INSERT INTO events_raw VALUES
(now() - INTERVAL 4 SECOND, 101, 'US', 'view', 1),
(now() - INTERVAL 3 SECOND, 101, 'US', 'click', 1),
(now() - INTERVAL 2 SECOND, 202, 'DE', 'view', 1),
(now() - INTERVAL 1 SECOND, 101, 'US', 'view', 1);
5. 롤업 조회하기
읽기 시점에 상태를 병합하거나 확정(finalize) 할 수 있어요:
- 읽기 시점에 병합
- -Final로 확정
SELECT
bucket_start,
country,
event_type,
uniqExactMerge(users_uniq) AS users,
sumMerge(value_sum) AS value_sum,
avgMerge(value_avg) AS value_avg,
countMerge(events_count) AS events
FROM events_rollup_1h
WHERE bucket_start >= now() - INTERVAL 1 DAY
GROUP BY ALL
ORDER BY bucket_start, country, event_type;
SELECT
bucket_start,
country,
event_type,
uniqExactMerge(users_uniq) AS users,
sumMerge(value_sum) AS value_sum,
avgMerge(value_avg) AS value_avg,
countMerge(events_count) AS events
FROM events_rollup_1h
WHERE bucket_start >= now() - INTERVAL 1 DAY
GROUP BY ALL
ORDER BY bucket_start, country, event_type
SETTINGS final = 1; -- or use SELECT ... FINAL
읽기가 항상 롤업을 친다고 예상한다면, 같은 1h 세분성의 "평범한" MergeTree 테이블에 확정된 숫자를 쓰는 두 번째 머티어리얼라이즈드 뷰를 만들 수 있어요. 상태는 더 큰 유연성을 주고, 확정된 숫자는 약간 더 단순한 읽기를 제공해요.
6. 최상 성능을 위해 기본 키의 필드로 필터링
EXPLAIN 명령으로 인덱스가 데이터를 어떻게 프루닝하는지 볼 수 있어요:
EXPLAIN indexes=1
SELECT *
FROM events_rollup_1h
WHERE bucket_start BETWEEN now() - INTERVAL 3 DAY AND now()
AND country = 'US';
┌─explain────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Expression │
3. │ ReadFromMergeTree (default.events_rollup_1h) │
4. │ Indexes: │
5. │ MinMax │
6. │ Keys: │
7. │ bucket_start │
8. │ Condition: and((bucket_start in (-Inf, 1758550242]), (bucket_start in [1758291042, +Inf))) │
9. │ Parts: 1/1 │
10. │ Granules: 1/1 │
11. │ Partition │
12. │ Keys: │
13. │ toYYYYMM(bucket_start) │
14. │ Condition: and((toYYYYMM(bucket_start) in (-Inf, 202509]), (toYYYYMM(bucket_start) in [202509, +Inf))) │
15. │ Parts: 1/1 │
16. │ Granules: 1/1 │
17. │ PrimaryKey │
18. │ Keys: │
19. │ bucket_start │
20. │ country │
21. │ Condition: and((country in ['US', 'US']), and((bucket_start in (-Inf, 1758550242]), (bucket_start in [1758291042, +Inf)))) │
22. │ Parts: 1/1 │
23. │ Granules: 1/1 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
위 쿼리 실행 계획은 MinMax 인덱스, 파티션 인덱스, 기본 키 인덱스라는 세 가지 유형의 인덱스가 사용되고 있음을 보여줘요. 각 인덱스는 기본 키에 지정된 필드 (bucket_start, country, event_type)를 사용해요. 최상의 필터링 성능을 위해 쿼리가 기본 키 필드를 사용해 데이터를 프루닝하도록 해야 해요.
7. 일반적인 변형
- 다른 세분성: 일별 롤업 추가:
CREATE TABLE events_rollup_1d
(
bucket_start Date,
country LowCardinality(String),
event_type LowCardinality(String),
users_uniq AggregateFunction(uniqExact, UInt64),
value_sum AggregateFunction(sum, Float64),
value_avg AggregateFunction(avg, Float64),
events_count AggregateFunction(count)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(bucket_start)
ORDER BY (bucket_start, country, event_type);
그리고 두 번째 머티어리얼라이즈드 뷰:
CREATE MATERIALIZED VIEW mv_events_rollup_1d
TO events_rollup_1d
AS
SELECT
toDate(event_time) AS bucket_start,
country,
event_type,
uniqExactState(user_id),
sumState(value),
avgState(value),
countState()
FROM events_raw
GROUP BY ALL;
- 압축: 원시 테이블의 큰 컬럼에 코덱 적용(예:
Codec(ZSTD(3))). - 비용 제어: 무거운 보존은 원시 테이블에 두고 장기간 롤업은 유지해요.
- 백필링: 과거 데이터를 로드할 때
events_raw에 삽입하고 머티어리얼라이즈드 뷰가 롤업을 자동으로 구축하도록 해요. 기존 행의 경우 적절하면 머티어리얼라이즈드 뷰 생성 시POPULATE를 사용하거나INSERT SELECT를 사용해요.
8. 정리와 보존
- 원시 TTL을 늘리고(예: 30/90일) 롤업은 더 오래 유지해요(예: 1년).
- 티어링이 활성화되어 있으면 TTL로 이동을 사용해 오래된 파트를 더 저렴한 스토리지로 옮길 수도 있어요.
9. 문제 해결
- 머티어리얼라이즈드 뷰가 업데이트되지 않나요? 삽입이(롤업 테이블이 아니라) events_raw로 가는지, 그리고 머티어리얼라이즈드 뷰 대상이 올바른지(
TO events_rollup_1h) 확인해요. - 느린 쿼리? 롤업을 치는지(롤업 테이블을 직접 조회)와 시간 필터가 롤업 세분성과 일치하는지 확인해요.
- 백필 불일치?
SYSTEM FLUSH LOGS를 사용하고system.query_log/system.parts를 확인해 삽입과 병합을 확인해요.