materialized_view 머티리얼라이즈는 기존 원본 테이블(source table)을 대상으로 하는 SELECT여야 합니다. PostgreSQL과 달리 ClickHouse의 materialized view는 “정적”이지 않으며, 이에 대응하는 REFRESH 작업도 없습니다. 대신 삽입 트리거처럼 동작하여, 원본 테이블에 삽입된 행에 정의된 SELECT 변환을 적용한 뒤 새 행을 대상 테이블(target table)에 삽입합니다. ClickHouse에서 materialized view가 작동하는 방식에 대한 자세한 내용은 ClickHouse materialized view 문서를 참조하십시오.
일반적인 머티리얼라이즈 개념과 공통 구성(engine, order_by, partition_by 등)은 Materializations 페이지를 참조하십시오.
대상 테이블 관리 방식
materialized_view 머티리얼라이즈를 사용하면 dbt-clickhouse는 변환된 행이 삽입될 materialized view와 대상 테이블을 모두 생성해야 합니다. 대상 테이블을 관리하는 방법은 두 가지입니다.
| 접근 방식 | 설명 | 상태 |
|---|---|---|
| 암시적 대상 | dbt-clickhouse가 동일한 모델 내에서 대상 테이블을 자동으로 생성하고 관리합니다. 대상 테이블 스키마는 MV의 SQL로부터 자동으로 추론됩니다. | 안정 |
| 명시적 대상 | 대상 테이블을 별도의 table 머티리얼라이즈로 정의한 다음, MV 모델에서 materialization_target_table() 매크로를 사용해 이를 참조합니다. MV는 해당 테이블을 가리키는 TO 절과 함께 생성됩니다. 이 기능은 dbt-clickhouse 버전 1.10부터 사용할 수 있습니다. 주의: 이 기능은 베타 상태이며 API는 커뮤니티 피드백에 따라 변경될 수 있습니다. | 베타 |
암시적 대상으로 머티리얼라이즈
materialized_view 모델을 정의하면 어댑터는 다음을 수행합니다.
- 모델 이름으로 대상 테이블을 생성합니다
- 이름이
<model_name>_mv인 ClickHouse materialized view를 생성합니다
SELECT 문에 있는 컬럼을 기반으로 자동 추론됩니다. 모든 리소스(대상 테이블 + MV)는 동일한 모델 구성을 공유합니다.
여러 materialized view
UNION을 구성하고, 각 materialized view의 SQL을 --my_mv_name:begin 및 --my_mv_name:end 형식의 주석으로 감싸면 됩니다.
예를 들어, 다음은 모두 모델의 동일한 대상 테이블에 데이터를 기록하는 두 개의 materialized view를 생성합니다. materialized view의 이름은 <model_name>_mv1 및 <model_name>_mv2 형식을 따릅니다:
대상 테이블 스키마를 갱신하는 방법
dbt run이 MV의 SQL에서 기존과 다른 컬럼을 감지할 때 대상 테이블 스키마를 어떻게 갱신할지 제어할 수 있습니다.
ignore 설정값). 하지만 이 설정을 변경하면 증분 모델에서 on_schema_change 구성과 동일하게 동작하도록 할 수 있습니다.
또한 이 설정은 안전장치로도 사용할 수 있습니다. 값을 fail로 설정하면 MV의 SQL에 있는 컬럼이 첫 번째 dbt run으로 생성된 대상 테이블과 다를 때 빌드가 실패합니다.
과거 데이터 채우기
catchup=True). 이 동작은 catchup 구성 값을 False로 설정하여 비활성화할 수 있습니다.
| 작업 | catchup: True (기본값) | catchup: False |
|---|---|---|
초기 배포 (dbt run) | 대상 테이블을 과거 데이터로 백필 | 대상 테이블을 빈 상태로 생성 |
전체 갱신 (dbt run --full-refresh) | 대상 테이블을 다시 만들고 백필 | 대상 테이블을 빈 상태로 다시 생성, 기존 데이터 손실 |
| 정상 운영 | materialized view가 새 삽입을 반영 | materialized view가 새 삽입을 반영 |
명시적 대상 테이블을 사용하는 머티리얼라이즈(베타)
- 모든 리소스(대상 테이블 + MV)는 동일한 구성을 공유합니다. 여러 MV가 동일한 대상 테이블을 가리키는 경우
UNION ALL구문을 사용해 함께 정의해야 합니다. - 이러한 리소스는 각각 별도로 관리할 수 없으며, 모두 동일한 model 파일에서 관리해야 합니다.
- 각 MV의 이름을 쉽게 제어할 수 없습니다.
- 모든 설정을 대상 테이블과 MV가 공유하므로 각 리소스를 개별적으로 구성하기 어렵고, 어떤 구성이 어느 리소스에 속하는지 파악하기도 어렵습니다.
table 머티리얼라이즈로 별도로 정의한 다음, materialized view model에서 이를 참조할 수 있습니다.
장점
- 완전히 분리된 리소스: 이제 각 리소스를 개별적으로 정의할 수 있어 가독성이 향상됩니다.
- dbt와 CH 간 1:1 리소스 대응: 이제 dbt 도구를 사용해 각각을 개별적으로 관리하고 수정해 나갈 수 있습니다.
- 서로 다른 구성 사용 가능: 이제 각 리소스에 서로 다른 구성을 적용할 수 있습니다.
- 더 이상 명명 규칙을 지킬 필요 없음: 이제 모든 리소스는 MV에
_mv를 덧붙인 사용자 지정 이름이 아니라, 지정한 이름으로 생성됩니다.
제한 사항
- 대상 테이블 정의는 dbt 방식과는 잘 맞지 않습니다. 원본 테이블에서 읽어 오는 SQL이 아니므로 여기서는 dbt 검증을 받을 수 없습니다. MV의 SQL은 계속 dbt 유틸리티로 검증되며, 대상 테이블 컬럼과의 호환성은 CH 수준에서 검증됩니다.
ref()함수의 제약과 관련해 몇 가지 문제를 확인했습니다: 모델 간 참조를 위해 이 함수를 사용해야 하지만, 업스트림 모델만 참조할 수 있고 다운스트림 모델은 참조할 수 없습니다. 이로 인해 이 구현에는 몇 가지 문제가 있습니다. dbt-core 리포지토리에 이슈를 등록했으며, 현재 가능한 해결책을 찾기 위해 논의 중입니다(dbt-labs/dbt-core#12319):ref()를 config 블록 안에서 호출하면 공유된 모델이 아니라 현재 모델을 반환합니다. 그래서 이를 config() 섹션에 정의할 수 없고, 이 의존성을 추가하려면 주석을 사용해야 합니다. 이 부분은 dbt 문서의 the “—depends_on:” approach에서 제시하는 동일한 패턴을 따르고 있습니다.ref()는 대상 테이블이 먼저 생성되도록 강제하므로 이 용도에는 동작하지만, 생성된 문서의 의존성 차트에서는 대상 테이블이 다운스트림이 아니라 또 다른 업스트림 의존성으로 표시되어 다소 이해하기 어렵습니다.unit-test역시 실제로는 읽지 않을 대상 테이블에 대해서도 일부 데이터를 정의하도록 강제합니다. 우회 방법은 해당 테이블의 데이터를 비워 두는 것입니다.
사용
events_daily.sql:
{{ materialization_target_table(ref('events_daily')) }} macro 호출은 MV의 대상 테이블을 구성합니다.
모델 page_events_aggregator.sql:
mobile_events_aggregator.sql 모델:
구성 옵션
materialized='table')에서:
| 옵션 | 설명 | 기본값 |
|---|---|---|
mv_on_schema_change | 테이블이 dbt에서 관리하는 MV에 사용될 때 스키마 변경을 처리하는 방법입니다. incremental 모델의 on_schema_change 구성과 동일하게 동작합니다. | 주의: materialized='table' 모델을 참조하는 MV가 없으면 평소와 같이 동작하므로, 이 설정을 정의해도 무시됩니다. 테이블이 MV의 대상인 경우에는 테이블 내부의 데이터를 보호하기 위해 이 구성의 기본값이 mv_on_schema_change='fail'로 설정됩니다. |
repopulate_from_mvs_on_full_refresh | --full-refresh 시 테이블의 SQL을 실행하는 대신, 이를 참조하는 모든 MV의 SQL을 사용해 INSERT-SELECT를 실행하여 테이블을 다시 빌드합니다. | False |
materialized='materialized_view')에서:
| 옵션 | 설명 | 기본값 |
|---|---|---|
catchup | MV 생성 시 과거 데이터를 백필할지 여부입니다. | True |
일반적으로는 MV에서
catchup만 True로 설정하거나, 해당 대상 테이블에서 repopulate_from_mvs_on_full_refresh만 True로 설정하는 것이 좋습니다. 둘 다 True로 설정하면 데이터가 중복될 수 있습니다.일반 작업
명시적 대상을 사용하는 전체 갱신
--full-refresh를 사용하면 명시적 대상 테이블이 다시 생성됩니다(따라서 이 과정에서 수집이 진행되면 데이터가 손실될 수 있습니다). 동작 방식은 구성에 따라 달라집니다.
옵션 1: 기본 --full-refresh 동작. 모든 항목이 다시 생성되지만, MV를 재생성하는 동안 대상 테이블은 비어 있거나 일부만 로드된 상태일 수 있습니다.
모든 항목을 삭제한 뒤 다시 생성합니다. MV SQL을 사용해 데이터를 다시 삽입하려면 catchup=True 설정을 유지하십시오:
catchup=False를 설정한 다음 MV에 대해 dbt run 또는 dbt run --full-refresh를 실행할 수 있습니다. 대상 테이블에서 --full-refresh를 실행하기 전에 MV가 먼저 생성되었는지 확인하십시오. 이 과정에서는 ClickHouse의 MV 정의를 사용합니다.
대상 테이블 모델에 repopulate_from_mvs_on_full_refresh=True를 설정하십시오. dbt run --full-refresh를 실행하면 다음이 수행됩니다.
- 새 임시 테이블을 생성합니다
- 각 MV의 SQL을 사용해 INSERT-SELECT를 실행합니다
- 테이블을 원자적으로 스왑합니다
대상 테이블 변경
--full-refresh 없이 MV의 대상 테이블은 변경할 수 없습니다. materialization_target_table() 참조를 변경한 뒤 일반 dbt run을 실행하면, 대상이 변경되었다는 오류 메시지와 함께 빌드가 실패합니다.
대상 테이블을 변경하려면 다음을 수행하세요:
materialization_target_table()호출을 업데이트합니다dbt run --full-refresh -s your_mv_model을 실행합니다
자주 발생하는 문제 해결
run 실행 중/실행 후 대상 테이블(target table)이 비어 있습니다
- materialized view가
catchup=False로 구성되어 있거나 대상 테이블이repopulate_from_mvs_on_full_refresh=False로 구성되어 있으면, materialized view가 생성되거나 대상 테이블이 다시 생성될 때 백필이 수행되지 않습니다. 이는 정상적인 동작입니다. 따라서 materialized view SQL을 사용해 데이터를 다시 삽입하려면 materialized view에서catchup=True(기본값)로 설정하거나 대상 테이블에서repopulate_from_mvs_on_full_refresh=True로 설정해야 합니다. 중복을 방지하려면 두 옵션을 동시에 활성화하지 마십시오. 자세한 내용은 구성 섹션을 참조하십시오. dbt run --full-refresh실행 중 materialized view가 기본값인catchup=True를 사용하면 대상이 다시 생성되고, MV가 데이터를 순차적으로 다시 삽입합니다. 이 상황을 방지하려면 명시적 대상을 사용하는 Full refresh를 참조하십시오.
dbt run --full-refresh는 repopulate_from_mvs_on_full_refresh=True가 설정된 대상 테이블에서 현재 프로젝트의 SQL이 아니라 이전 materialized view 버전의 로직을 사용합니다
repopulate_from_mvs_on_full_refresh=True는 ClickHouse에 이미 정의된 기존 MV SQL을 사용합니다. 새 materialized view 정의를 사용하게 하려면 대상 테이블에서 dbt run --full-refresh를 실행하기 전에 각 materialized view에 대해 dbt run을 실행하십시오.
run 실행 후 중복 데이터가 발생합니다
- materialized view에서
catchup=True가 활성화되어 있고, 대상 테이블에서도repopulate_from_mvs_on_full_refresh=True가 활성화되어 있을 수 있습니다. 실행하려는 작업에 따라 둘 중 하나만 사용하십시오. 자세한 내용은 구성 섹션을 확인하십시오. - 대상 테이블이
WHERE 0으로 정의되지 않았습니다. 대상 테이블은 비어 있는 상태로 생성되어야 하지만WHERE 0이 포함되지 않으면 내부 쿼리가 데이터를 삽입할 수 있습니다. 해당 절이 포함되어 있는지 확인하십시오.
dbt run --full-refresh 실행 후 활성 수집 중 데이터 손실
dbt run --full-refresh를 실행한 후 원본 테이블의 일부 행이 대상 테이블에 누락될 수 있습니다.
ClickHouse materialized view는 삽입 트리거처럼 동작하므로, 존재하는 동안에만 데이터를 포착합니다. full refresh 중에는 MV가 삭제되었다가 다시 생성되는 짧은 구간(“blind window”)이 발생합니다. 이 구간 동안 원본 테이블에 삽입된 행은 포착되지 않습니다. 자세한 내용은 활성 수집 중 동작 섹션을 확인하십시오.
디버깅 방법
ClickHouse에서 MV의 현재 대상 확인
system.tables를 쿼리합니다:
dbt가 테이블을 materialized view 대상 테이블로 인식하는지 확인
Table <table_name> is used as a target by a dbt-managed materialized view. Defaulting mv_on_schema_change to “fail” to prevent data loss.
이 메시지가 표시되면 dbt가 해당 테이블을 하나 이상의 dbt 관리 materialized view가 대상으로 사용하는 것으로 감지한 것입니다. 이 메시지가 표시되어야 하는데 보이지 않는다면, 다음 사항을 확인하십시오:
- materialized view model에서
{{ materialization_target_table(ref('your_target')) }}를 올바르게 정의했는지 - materialized view model의 구성에
materialized='materialized_view'가 포함되어 있는지 - materialized view와 대상 테이블이 모두 최소 1회 이상 실행되었는지
암시적 대상 방식에서 명시적 대상 방식으로 마이그레이션
materialized='table' 새 모델 파일을 만드십시오. 빈 테이블을 생성하려면 WHERE 0 절을 사용하십시오. 현재 암시적 대상 materialized view 모델과 동일한 이름을 사용하십시오. 이제 이 모델을 사용해 대상 테이블을 반복적으로 수정할 수 있습니다.
materialization_target_table() macro 호출을 포함하는 새 model을 생성합니다. 이전에 UNION ALL을 사용했다면 해당 부분과 comment를 제거합니다.
model 이름은 다음 명명 규칙을 따라야 합니다:
- MV가 하나만 정의된 경우 이름은
<old_model_name>_mv입니다 - 여러 MV가 정의된 경우 각각의 이름은
<old_model_name>_mv_<name_in_comments>입니다
my_model.sql 예시(암시적 대상, UNION ALL이 포함된 단일 model):
암시적 대상 방식과 명시적 대상 방식의 동작 비교
전반적인 동작 방식
| 작업 | 암시적 대상 | 명시적 대상 |
|---|---|---|
| 첫 번째 dbt 실행 | 모든 리소스가 생성됨 | 모든 리소스가 생성됨 |
| 다음 dbt 실행 | 개별 리소스를 따로 관리할 수 없으며, 모두 함께 처리됩니다: 대상 테이블(target table): 변경 사항은 on_schema_change 설정으로 관리됩니다. 기본값은 ignore이므로 새 컬럼은 처리되지 않습니다.materialized view: 모두 alter table modify query 작업으로 업데이트됩니다 | 변경 사항을 개별적으로 적용할 수 있습니다: 대상 테이블(target table): dbt에서 정의한 materialized view의 대상 테이블인지 자동으로 감지합니다. 대상 테이블인 경우 컬럼 변경은 기본적으로 mv_on_schema_change 설정으로 관리되며, 기본값은 fail이므로 컬럼이 변경되면 실패합니다. 이 기본값은 보호 장치로 추가되었습니다materialized view: SQL이 alter table modify query 작업으로 업데이트됩니다. |
| dbt run —full-refresh | 개별 리소스를 따로 관리할 수 없으며, 모두 함께 처리됩니다: 대상 테이블(target table): 대상 테이블이 비어 있는 상태로 다시 생성됩니다. 모든 materialized view의 SQL을 함께 사용해 backfill을 구성할 수 있도록 catchup을 사용할 수 있습니다. 기본적으로 catchup은 True입니다materialized view: 모두 다시 생성됩니다. | 변경 사항이 개별적으로 적용됩니다: 대상 테이블(target table): 일반적인 방식대로 다시 생성됩니다. materialized view: 삭제 후 다시 생성됩니다. 초기 backfill을 위해 catchup을 사용할 수 있습니다. 기본적으로 catchup은 True입니다. 참고: 이 과정에서 materialized view가 다시 생성될 때까지 대상 테이블은 비어 있거나 일부만 로드된 상태일 수 있습니다. 이를 방지하려면 대상 테이블을 단계적으로 갱신하는 방법을 설명하는 다음 섹션을 확인하십시오. |
수집이 진행 중일 때의 동작
- ClickHouse materialized view는 insert trigger로 동작하므로, 존재하는 동안에만 데이터를 포착합니다. materialized view가 삭제되었다가 다시 생성되면(예:
--full-refresh중) 그 사이 원본 테이블에 삽입된 행은 materialized view에서 처리되지 않습니다. 이를 materialized view가 “blind” 상태라고 합니다. - 각
catchup프로세스는 모두 materialized view SQL을 사용하는INSERT INTO ... SELECT작업을 기반으로 하며, materialized view의 동작 방식과는 별개입니다.INSERT가 시작된 후 새로 들어오는 데이터는 여기에 포착되지 않지만, attached 상태인 materialized view에는 포착됩니다.
암시적 대상 작업
| 작업 | 내부 프로세스 | 삽입 중 안전성 |
|---|---|---|
첫 번째 dbt run | 1. 대상 테이블 생성 2. 데이터 삽입 ( catchup=True인 경우)3. materialized view 생성 | ⚠️ 1단계와 3단계 사이에는 materialized view가 데이터를 포착하지 못합니다. 이 구간 동안 소스에 삽입된 모든 행은 수집되지 않습니다. |
이후 dbt run | ALTER TABLE ... MODIFY QUERY | ✅ 안전합니다. materialized view가 원자적으로 갱신됩니다. |
dbt run --full-refresh | 1. 백업 테이블 생성 2. 데이터 삽입 ( catchup=True인 경우)3. materialized view 삭제 4. 테이블 교환 5. materialized view 재생성 | ⚠️ materialized view를 재생성하는 동안에는 데이터를 포착하지 못합니다. 3단계와 5단계 사이에 소스에 삽입된 데이터는 새 대상 테이블에 나타나지 않습니다. |
명시적 대상 작업
| 작업 | 내부 프로세스 | 삽입이 진행 중일 때의 안전성 |
|---|---|---|
첫 번째 dbt run | 1. MV 생성 (TO 절 포함)2. catch-up 실행 ( catchup=True인 경우) | ✅ MV가 먼저 생성되므로 새 삽입이 즉시 포착됩니다. ⚠️ catch-up으로 데이터가 중복될 수 있습니다 — backfill 쿼리가 이미 MV에서 처리 중인 행과 겹칠 수 있습니다. 중복 제거 엔진(예: ReplacingMergeTree)을 사용하는 경우 안전합니다. |
후속 dbt run | ALTER TABLE ... MODIFY QUERY | ✅ 안전합니다. MV가 원자적으로 갱신됩니다. |
MV에 대한 dbt run --full-refresh | 1. MV 삭제 후 재생성 2. catch-up 실행 ( catchup=True인 경우) | ⚠️ 재생성 중에는 MV가 데이터를 포착하지 못합니다 (삭제와 생성 사이). ⚠️ 삽입이 동시에 발생하면 catch-up으로 데이터가 중복될 수 있습니다. |
| 작업 | 내부 프로세스 | 삽입이 진행 중일 때의 안전성 |
|---|---|---|
dbt run | mv_on_schema_change 설정에 따라 스키마 변경 적용 | ✅ 안전합니다. 데이터 이동이 없습니다. |
dbt run --full-refresh (기본값) | 테이블 재생성(빈 상태로 유지됨) | ⚠️ MV가 backfill할 때까지 대상 테이블이 비어 있습니다. 새 테이블이 생성되면 MV는 계속 해당 테이블에 삽입합니다. |
repopulate_from_mvs_on_full_refresh=True를 사용한 dbt run --full-refresh | 1. 백업 테이블 생성 2. 각 MV의 SQL을 사용해 데이터 삽입 3. 테이블을 원자적으로 교환 | ⚠️ 재생성 중에는 MV가 데이터를 포착하지 못합니다. 1단계와 3단계 사이에 삽입된 데이터는 새 테이블에 나타나지 않습니다. 이는 다음 버전에서 변경될 수 있습니다. |
갱신 가능 materialized view
refreshable 구성 객체를 추가하십시오:
| 옵션 | 설명 | 필수 여부 | 기본값 |
|---|---|---|---|
| refresh_interval | 인터벌 절(필수) | 예 | |
| randomize | 무작위화 절이며, RANDOMIZE FOR 뒤에 추가됩니다 | ||
| append | True로 설정하면 갱신할 때마다 기존 행을 삭제하지 않고 테이블에 행을 삽입합니다. 이 삽입은 일반적인 INSERT SELECT와 마찬가지로 원자적이지 않습니다. | False | |
| depends_on | 갱신 가능 MV의 종속성 목록입니다. 종속성은 {schema}.{view_name} 형식으로 지정하십시오 | ||
| depends_on_validation | depends_on에 지정한 종속성의 존재 여부를 검증할지 여부입니다. 종속성에 스키마가 포함되지 않은 경우 검증은 default 스키마에서 수행됩니다 | False |
암시적 대상 예시
대상을 명시적으로 지정하는 예시
제한 사항
- 종속성이 있는 갱신 가능 materialized view(MV)를 ClickHouse에서 생성할 때, 생성 시점에 지정한 종속성이 존재하지 않더라도 ClickHouse는 오류를 발생시키지 않습니다. 대신 갱신 가능 MV는 비활성 상태로 유지되며, 종속성이 충족되어 업데이트 처리를 시작하거나 갱신할 수 있을 때까지 대기합니다. 이는 의도된 동작이지만, 필요한 종속성을 제때 해결하지 않으면 데이터 가용성이 지연될 수 있습니다. 따라서 갱신 가능 materialized view를 생성하기 전에 모든 종속성이 올바르게 정의되어 있고 실제로 존재하는지 확인해야 합니다.
- 현재로서는 mv와 해당 종속성 사이에 실제 “dbt 연동”이 없으므로 생성 순서는 보장되지 않습니다.
- 갱신 가능 기능은 여러 mv가 동일한 대상 모델을 가리키는 경우에 대해서는 테스트되지 않았습니다.