본문으로 건너뛰기
clickhouse검색
핵심 구조

머티리얼라이즈드 뷰 (증분 MV, Refreshable MV, 연쇄 MV)

INSERT 트리거로 동작하는 증분 머티리얼라이즈드 뷰와 주기적으로 전체 쿼리를 다시 실행하는 Refreshable 머티리얼라이즈드 뷰의 차이, 집계 롤업과 데이터 라우팅 패턴, 연쇄와 증분 리프레시 같은 최근 변화, 운영에서 겪는 함정을 정리합니다.

Materialized ViewRefreshable MV롤업증분 집계데이터 라우팅2026-10 기준, 약 11분상태 표시 설명

두 종류의 MV

ClickHouse의 머티리얼라이즈드 뷰(MV)는 다른 DB의 MV와 다릅니다. 종류가 둘이고 동작 원리가 완전히 다릅니다.

구분증분(incremental) MVRefreshable MV
동작원본 테이블에 INSERT 되는 블록마다 뷰 쿼리를 실행해 결과를 대상 테이블에 넣는 트리거정해진 주기(또는 의존 MV 완료 후)에 뷰 쿼리를 전체 실행해 대상 테이블을 교체 또는 추가
과거 데이터보지 않음 (INSERT 블록만)매번 전체(또는 증분 모드)
조인가능하지만 INSERT 블록 기준으로만 조인됨자유
지연적재 즉시주기만큼
용도롤업, 라우팅, 변환 적재보고용 집계, 조인 결과 스냅숏, 덤프 테이블

증분 MV는 ClickHouse의 간판 기능입니다. 적재 경로에 끼어들어 집계, 변환, 분기를 공짜에 가깝게 처리합니다.

증분 MV 기본 패턴

1. 롤업 (집계 테이블 채우기)

CREATE TABLE events_hourly
(
    hour   DateTime,
    event  LowCardinality(String),
    cnt    UInt64,
    users  AggregateFunction(uniq, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (hour, event);

CREATE MATERIALIZED VIEW events_hourly_mv TO events_hourly AS
SELECT toStartOfHour(ts) AS hour, event, count() AS cnt, uniqState(user_id) AS users
FROM events
GROUP BY hour, event;
  • TO 대상테이블 형식을 쓰세요. 대상 테이블을 명시하지 않으면 .inner.<uuid> 이름의 숨은 테이블이 생겨 운영이 번거롭습니다.
  • INSERT 블록 안에서 GROUP BY가 한 번 실행되고 결과가 대상 테이블로 들어갑니다. 블록마다 행이 생기므로 대상 테이블은 머지 때 더 합쳐지는 Summing 또는 Aggregating 엔진이어야 합니다. 읽을 때는 sum, -Merge로 다시 집계합니다(MergeTree 가족).

2. 데이터 라우팅과 변환

-- 원본은 Null 엔진: 저장하지 않고 MV 로만 흘려보냄
CREATE TABLE raw_logs (line String) ENGINE = Null;

CREATE MATERIALIZED VIEW parse_logs_mv TO logs AS
SELECT parseDateTimeBestEffort(extract(line, '^\\S+')) AS ts,
       extract(line, '\\[(\\w+)\\]') AS level,
       line AS message
FROM raw_logs
WHERE line != '';

CREATE MATERIALIZED VIEW error_logs_mv TO error_logs AS
SELECT * FROM raw_logs WHERE line LIKE '%ERROR%';

하나의 원본에 MV를 여럿 걸어 여러 대상 테이블로 분기할 수 있습니다. Kafka 엔진 테이블에서 MergeTree로 옮기는 전형적 구조도 이것입니다(Kafka와 스트리밍 수집).

3. 다른 정렬 키의 복사본

같은 데이터를 ORDER BY user_id로도 갖고 싶을 때 MV로 두 번째 테이블을 채웁니다. 다만 같은 테이블 안에서 해결되는 프로젝션이 보통 더 단순합니다.

증분 MV를 쓸 때 꼭 알아야 할 것

  1. 원본 테이블의 기존 데이터는 들어가지 않습니다. MV 생성 뒤 INSERT 되는 블록만 처리합니다. 과거분은 INSERT INTO 대상 SELECT ... FROM 원본 WHERE ts < '생성 시각'으로 직접 채웁니다. POPULATE 옵션은 생성 중 들어오는 데이터를 놓칠 수 있어 운영 테이블에서는 피합니다.
  2. 원본에 대한 머지, 뮤테이션, 삭제는 MV에 반영되지 않습니다. 원본에서 행을 지워도 롤업 테이블 값은 그대로입니다. 재적재가 필요하면 해당 기간을 대상에서 지우고 다시 넣습니다.
  3. MV 쿼리는 INSERT 블록만 봅니다. SELECT ... FROM events WHERE user_id IN (SELECT ... FROM events) 같은 자기 참조는 블록 안에서만 평가됩니다. 조인의 오른쪽 테이블은 매 블록마다 전체를 읽으므로 작은 테이블(또는 딕셔너리)만 조인합니다.
  4. MV 실행이 실패하면 원본 INSERT도 실패합니다(기본). materialized_views_ignore_errors = 1로 바꿀 수 있지만 데이터가 조용히 빠지므로 신중하게.
  5. 열 이름을 맞춰야 합니다. 대상 테이블 열과 MV SELECT의 별칭이 이름으로 매칭됩니다. 위치가 아닙니다. 대상에 없는 열은 버려지고, SELECT에 없는 대상 열은 기본값이 들어갑니다.
  6. MV 안의 쿼리는 INSERT 비용에 더해집니다. 복잡한 정규식 파싱이나 큰 조인은 적재 처리량을 떨어뜨립니다. 적재가 느려지면 system.query_views_log(켜져 있다면)와 system.part_log로 MV 비용을 확인합니다.
  7. 원본 열을 바꾸면 MV가 깨질 수 있습니다. 열을 지우거나 이름을 바꾸기 전에 system.tables의 dependencies_table로 걸려 있는 MV를 확인합니다.

주의 SELECT *를 MV에 쓰지 마세요. 원본에 열이 추가되면 대상과 맞지 않아 INSERT가 실패합니다. 열을 명시적으로 나열합니다.

여러 MV의 실행 순서

한 원본에 MV가 여럿이면 기본적으로 생성된 순서(이름순이 아님) 로 순차 실행되고, parallel_view_processing = 1이면 병렬입니다. 한 MV가 실패하면 뒤의 MV는 실행되지 않고 전체 INSERT가 실패합니다.

MV에서 MV로 (연쇄)

MV의 대상 테이블을 또 다른 MV의 원본으로 삼을 수 있습니다. 2단 롤업(분 단위에서 시간 단위)에 쓰입니다. 깊어질수록 적재 지연과 디버깅 난도가 오르니 2단까지를 권장합니다.

Refreshable MV

주기적으로 전체 쿼리를 실행하는 방식입니다. 조인이 자유롭고, 원본의 삭제나 갱신도 다음 리프레시에 반영됩니다. 24.x 사이에 Exp로 들어와 지금은 운영에 쓰이는 단계이며, 26.x에서 연쇄(cascading)와 증분(incremental) 리프레시가 더해졌습니다.

CREATE MATERIALIZED VIEW daily_report
REFRESH EVERY 1 DAY OFFSET 2 HOUR
TO daily_report_table
AS
SELECT d.day, d.event, d.cnt, p.plan
FROM events_daily AS d
LEFT JOIN users AS p ON d.user_id = p.user_id;

-- 다른 MV 가 끝난 뒤 실행 (연쇄)
CREATE MATERIALIZED VIEW weekly_report
REFRESH AFTER 10 MINUTE DEPENDS ON daily_report
TO weekly_report_table AS SELECT ...;
옵션뜻
REFRESH EVERY 1 HOUR [OFFSET 5 MINUTE]정각 기준 주기 실행
REFRESH AFTER 30 MINUTE이전 실행 완료 후 간격
DEPENDS ON mv1, mv2지정한 리프레시 MV가 끝난 뒤 실행
APPEND대상 테이블을 교체하지 않고 결과를 추가 (스냅숏 누적)
APPEND INCREMENTAL (26.9)마지막 리프레시 이후 커밋된 행만 처리해 추가
RANDOMIZE FOR 10 MINUTE실행 시각을 분산
SETTINGS refresh_retries = 3실패 재시도
-- 상태 확인과 수동 실행
SELECT view, status, last_success_time, next_refresh_time, exception
FROM system.view_refreshes;
SYSTEM REFRESH VIEW daily_report;
SYSTEM STOP VIEW daily_report;   -- 일시 정지
  • 기본(교체) 모드는 새 결과를 임시 테이블에 만든 뒤 원자적으로 교체하므로 리프레시 중에도 이전 결과를 읽을 수 있습니다.
  • 복제 환경에서는 복제본 가운데 하나가 실행하고 결과를 Replicated 대상 테이블로 공유합니다. Cloud에서도 같은 방식입니다.
  • 매번 전체를 다시 계산하므로 원본이 수십억 행이면 비용이 큽니다. 그런 경우 증분 MV로 중간 집계를 만들고 Refreshable MV는 그 위에서 조인만 하도록 층을 나눕니다.

증분 MV와 Refreshable MV 고르기

요구선택
적재 즉시 반영되는 롤업증분 MV + Aggregating/SummingMergeTree
원본 삭제, 갱신이 반영되어야 함Refreshable MV
큰 테이블끼리 조인한 결과Refreshable MV
외부 소스(PostgreSQL 엔진, S3) 스냅숏Refreshable MV
적재 경로에서 파싱, 분기증분 MV
날마다 쌓이는 보고서 이력Refreshable MV APPEND
최근 커밋분만 처리하는 append 로그 파생Refreshable MV APPEND INCREMENTAL

운영 쿼리 모음

-- 어떤 테이블에 어떤 MV 가 걸려 있나
SELECT name, engine, dependencies_table
FROM system.tables
WHERE database = currentDatabase() AND notEmpty(dependencies_table);

-- MV 정의 보기
SHOW CREATE TABLE events_hourly_mv;

-- MV 쿼리 바꾸기: 대상 테이블은 그대로 두고 MV 만 재생성
DROP VIEW events_hourly_mv;
CREATE MATERIALIZED VIEW events_hourly_mv TO events_hourly AS SELECT ...;

-- 또는 (대상 테이블을 TO 로 가진 MV 는 쿼리만 수정 가능)
ALTER TABLE events_hourly_mv MODIFY QUERY SELECT ...;

DROP VIEW와 재생성 사이에 들어온 데이터는 롤업에서 빠집니다. 적재를 잠깐 멈추거나, 재생성 뒤 그 구간을 INSERT ... SELECT로 보충합니다. MODIFY QUERY는 중단 없이 바꿀 수 있어 더 안전합니다.

체크리스트

  1. 모든 MV는 TO 대상테이블을 쓰고, 대상은 Summing 또는 Aggregating 엔진이다.
  2. MV SELECT에 열을 명시했고 SELECT *가 없다.
  3. 생성 시점 이전 데이터를 수동으로 채웠다.
  4. 원본 삭제, 재적재 때 대상 테이블을 함께 정리하는 절차가 있다.
  5. 조인은 작은 테이블 또는 딕셔너리만 쓴다.
  6. 전체 재계산이 필요한 보고서는 Refreshable MV로 두고 system.view_refreshes를 모니터링한다.

공식 문서

이 문서에서