커스텀 증분화(Custom incrementalization)
커스텀 증분화(Custom incrementalization)
표준 갱신 모드(INCREMENTAL, FULL)가 변환을 표현할 수 없을 때, 커스텀 증분화를 사용하면 Snowflake가 매 갱신마다 실행하는 MERGE 또는 INSERT 로직을 직접 작성할 수 있어요. 변경 사항이 동적 테이블에 도달하는 방식을 정확히 정의해요. Snowflake가 스케줄링, 재시도, 트랜잭션 보장을 처리해요.
출처: Snowflake 문서
본문
커스텀 증분화를 사용할 때
SELECT 기반 동적 테이블이 필요한 변환을 표현할 수 없을 때 커스텀 증분화를 고려하세요.
| 측면 | SELECT 기반 동적 테이블 | 커스텀 증분 동적 테이블 |
|---|---|---|
| 로직 유형 | 선언형 SELECT | 명령형 MERGE 또는 INSERT |
| 증분 전략 | Snowflake가 자동 추론 | 사용자 정의 |
| 의미론 | 지연 뷰 동등성 | 사용자 정의 (시스템 보장 없음) |
| 가장 적합한 대상 | SELECT로 표현 가능한 변환 | 삭제가 있는 CDC, 스트림-정적 조인, 감사 추적 |
| 스트림·태스크에서 마이그레이션 | SELECT로 재작성 필요 | 단일 문 MERGE 또는 INSERT 패턴 허용 |
커스텀 증분화에 적합한 시나리오:
- SELECT로 표현할 수 없는 쿼리: 스트림-정적 조인, 시간 범위 중복 제거 로직, 또는 단일
SELECT가 포착할 수 없는 조건 분기. - 업데이트·삭제 의미론에 대한 명시적 제어: 소프트 삭제, 조건부 업데이트, 또는 행별로 업데이트·삭제·건너뛸지 결정해야 하는 병합 로직.
- 사용자 정의 출력 의미론: 감사 추적, 누산기(accumulator), 시점 스냅샷.
- 상태 재사용과 메모이제이션: 과거 데이터를 다시 스캔하지 않도록 갱신 간 중간 결과를 누적. 예를 들어 이전 결과와 새 변경을 결합해 실행 중인 top-K 리더보드를 유지. Top-K 리더보드 문서 참조.
- 스트림·태스크에서 마이그레이션: 단일 문
MERGE또는INSERT패턴이REFRESH USING으로 직접 이식돼요.SELECT로 재작성하지 않고 관리형 스케줄링과 모니터링을 얻어요. 저장 프로시저나 다중 문 트랜잭션을 사용하는 파이프라인은 단일 지원 DML 문으로 재구성해야 해요.
구문(Syntax)
CREATE [OR REPLACE] DYNAMIC TABLE <name> (
<col_name> <col_type> [, ...]
)
TARGET_LAG = {'<time_spec>' | DOWNSTREAM}
WAREHOUSE = <warehouse_name>
[REFRESH_MODE = {AUTO | CUSTOM_INCREMENTAL}]
[INITIALIZE = ON_SCHEDULE]
[BACKFILL FROM <table_name>]
[START AT ({STREAM => '<stream_name>' | TIMESTAMP => <timestamp> | STATEMENT => <query_id> | OFFSET => -<seconds>})]
REFRESH USING (<dml_statement>)
핵심 요구 사항:
- 명시적 컬럼 목록이 필요해요. Snowflake는 DML에서 스키마를 추론할 수 없어요.
REFRESH USING이 있으면REFRESH_MODE = AUTO는CUSTOM_INCREMENTAL로 결정돼요.REFRESH USING블록당 DML 문은 하나만 허용돼요(다중 문 트랜잭션 없음).
MERGE INTO SELF
새 행 삽입 외에도 기존 행을 업데이트하거나 삭제해야 할 때 MERGE INTO SELF를 사용하세요. SELF는 생성 중인 동적 테이블을 가리키며 별칭을 지정할 수 있어요.
REFRESH USING (
MERGE INTO SELF [AS <target_alias>]
USING (
SELECT ... FROM <source> CHANGES ([INFORMATION => {DEFAULT | APPEND_ONLY}]) [AS <alias>]
[JOIN <table> ON ...]
[WHERE ...]
[QUALIFY ...]
) AS <source_alias>
ON <join_condition>
[WHEN MATCHED [AND ...] THEN {UPDATE SET ... | DELETE}]
[...]
[WHEN NOT MATCHED [AND ...] THEN INSERT (...) VALUES (...)]
)
USING 서브쿼리에서 SELF를 읽어 수신 변경과 동적 테이블의 현재 내용을 조인할 수 있어요. REFRESH USING 안에서는 동적 테이블을 객체 이름으로 참조할 수 없어요.
INSERT INTO SELF
기존 행을 수정할 필요가 없는 append-only 패턴에는 INSERT INTO SELF를 사용하세요.
REFRESH USING (
INSERT INTO SELF
SELECT ... FROM <source> CHANGES ([INFORMATION => {DEFAULT | APPEND_ONLY}]) [AS <alias>]
[JOIN <table> ON ...]
[WHERE ...]
)
INSERT INTO SELF는 행을 추가만 해요. 기존 행을 업데이트하거나 삭제해야 한다면 MERGE를 사용하세요.
CHANGES 절
CHANGES 절은 스트림 의미론을 대체해요. Snowflake가 변경 간격을 갱신 경계에 자동으로 바인딩하므로 시간 범위(AT, BEFORE, END)를 지정하지 않아요.
제약 사항:
SELF에는CHANGES를 사용할 수 없어요(Snowflake가 오류 반환).- 기본 테이블에 변경 추적이 활성화되어 있어야 해요. 커스텀 증분 동적 테이블을 만들면 기본 테이블에 변경 추적을 활성화하려고 시도해요.
- 시간 범위를 지정할 수 없어요. 간격은 Snowflake가 자동으로 관리해요.
INFORMATION 모드는 어떤 변경 유형이 보이는지 제어해요. INFORMATION을 지정하지 않으면 DEFAULT 모드가 사용돼요(예: CHANGES()).
DEFAULT: 삽입, 업데이트, 삭제.APPEND_ONLY: 삽입만.
변경 집합에서 사용 가능한 메타데이터 컬럼:
| 컬럼 | 설명 |
|---|---|
METADATA$ACTION |
'INSERT' 또는 'DELETE'(업데이트는 DELETE + INSERT 쌍으로 나타남) |
METADATA$ISUPDATE |
행이 UPDATE 작업의 일부이면 TRUE |
삽입 전에 메타데이터 컬럼을 제거하려면 SELECT * EXCLUDE(METADATA$ACTION, METADATA$ISUPDATE)를 사용하세요.
SELF 키워드
SELF는 REFRESH USING 안에서 두 가지 역할을 해요:
- 쓰기 target:
MERGE INTO SELF또는INSERT INTO SELF. - 읽기 소스:
USING서브쿼리의FROM SELF AS cur는 동적 테이블의 현재 내용을 읽어요.
REFRESH USING 안에서 동적 테이블을 객체 이름으로 참조할 수 없어요. SELF만 사용하세요. SELF에 대한 CHANGES()는 허용되지 않아요.
BACKFILL FROM과 START AT
이 절들은 동적 테이블이 초기에 어떻게 채워지고 이후 갱신이 어디서 변경을 읽기 시작하는지 제어해요.
BACKFILL FROM이 없으면 초기 갱신이 소스의 전체 변경 기록에 대해 REFRESH USING 문을 실행해요. CHANGES()는 초기 실행에서 모든 기존 행을 INSERT로 취급하므로, 초기 갱신은 사실상 소스의 모든 행을 MERGE 또는 INSERT 로직으로 재생해요. 큰 기본 테이블에서는 비싸고 느릴 수 있어요.
BACKFILL FROM은 더 빠른 대안을 제공해요: Snowflake가 REFRESH USING 로직을 실행하지 않고 기존 테이블에서 클론해 동적 테이블을 채워요. 이후 갱신은 백필 후 도착하는 새 변경만 처리해요.
| 구성 | 초기 채우기 | 이후 갱신 시작 |
|---|---|---|
| 둘 다 없음 (기본) | 초기 갱신이 REFRESH USING을 통해 모든 기존 소스 행을 INSERT로 처리 |
생성 시점부터 |
BACKFILL FROM <table> |
백필 테이블에서 클론 (REFRESH USING 우회) |
생성 시점부터 |
BACKFILL FROM + START AT |
백필 테이블에서 클론 (REFRESH USING 우회) |
START AT 지점부터 |
START AT는 네 가지 옵션을 받아들여요:
TIMESTAMP => <timestamp>: 특정 시점.STATEMENT => <query_id>: 특정 쿼리가 완료된 후의 지점.STREAM => <stream_name>: 기존 스트림의 현재 오프셋.OFFSET => -<seconds>: 현재 시간에서의 음수 초 오프셋.
BACKFILL FROM과 START AT는 생성 시점에만 설정할 수 있어요. ALTER DYNAMIC TABLE 또는 CREATE OR ALTER DYNAMIC TABLE로 추가하거나 변경할 수 없어요.
예시(Examples)
스트림-정적 조인이 있는 append-only 강화 (INSERT INTO)
이 예시는 새 클릭 이벤트를 페이지 메타데이터로 강화해요. 클릭 테이블은 append-only이고, 페이지는 정적 차원 테이블이에요.
CREATE OR REPLACE DYNAMIC TABLE dt_enriched_clicks (
click_id INT,
user_id INT,
page_title STRING,
section STRING,
click_ts TIMESTAMP
)
TARGET_LAG = '1 minute'
WAREHOUSE = transform_wh
REFRESH USING (
INSERT INTO SELF
SELECT c.click_id, c.user_id, p.page_title, p.section, c.click_ts
FROM clicks CHANGES(INFORMATION => APPEND_ONLY) AS c
LEFT OUTER JOIN pages AS p ON c.page_id = p.page_id
);
마지막 갱신 이후의 새 클릭만 처리돼요. LEFT OUTER JOIN은 과거 데이터를 다시 스캔하지 않고 각 클릭을 페이지 제목과 섹션으로 강화해요. 업데이트와 삭제를 처리하는 전체 CDC 변형은 MERGE를 사용한 스트림-정적 조인 문서를 참조하세요.
감사 삭제 로그 (INSERT INTO)
이 예시는 users 테이블의 독립 삭제를 감사 로그로 포착하고, 업데이트의 삭제 절반을 걸러 냅니다.
CREATE OR REPLACE DYNAMIC TABLE dt_deletions_log (
id INT,
name STRING,
email STRING
)
TARGET_LAG = '1 minute'
WAREHOUSE = transform_wh
INITIALIZE = ON_SCHEDULE
REFRESH USING (
INSERT INTO SELF
SELECT * EXCLUDE (METADATA$ISUPDATE, METADATA$ACTION)
FROM users CHANGES(INFORMATION => DEFAULT)
WHERE NOT METADATA$ISUPDATE
AND METADATA$ACTION = 'DELETE'
);
DEFAULT 정보 모드는 모든 변경 유형(삽입, 업데이트, 삭제)을 노출해요. WHERE 절은 METADATA$ISUPDATE가 TRUE인 행을 제외해 독립 삭제만 유지해요. EXCLUDE는 삽입 전에 메타데이터 컬럼을 제거해요.
append-only 변경에 대한 증분 집계
이 예시는 플레이어 점수의 실행 총계를 유지해요. 각 갱신은 새 행만 합산해 기존 총계에 더해요.
CREATE OR REPLACE DYNAMIC TABLE dt_player_scores (
player_id INT,
total_score INT
)
TARGET_LAG = '1 minute'
WAREHOUSE = transform_wh
REFRESH USING (
MERGE INTO SELF AS tgt
USING (
SELECT player_id, SUM(score) AS batch_score
FROM match_results CHANGES(INFORMATION => APPEND_ONLY)
GROUP BY player_id
) AS src
ON tgt.player_id = src.player_id
WHEN MATCHED THEN
UPDATE SET tgt.total_score = tgt.total_score + src.batch_score
WHEN NOT MATCHED THEN
INSERT (player_id, total_score) VALUES (src.player_id, src.batch_score)
);
기존 플레이어는 배치 합계를 더해 점수가 업데이트돼요. 새 플레이어는 첫 배치 점수로 삽입돼요. 이렇게 하면 모든 갱신에서 전체 match_results 기록을 다시 스캔하는 것을 피해요.
이 예시는 APPEND_ONLY 모드를 사용하므로 삽입만 처리돼요. 소스의 삭제와 업데이트는 무시돼요. 삭제를 처리해야 한다면 DEFAULT 모드를 사용하세요.
사용 참고 사항
- 각 갱신은 단일 자동 커밋 트랜잭션으로 실행돼요.
REFRESH USING문이 실패하면 전체 갱신이 롤백돼요. - 다음 갱신 전에
CHANGES()를 통해 쿼리되는 기본 테이블의 데이터 보존이 만료되면 갱신이 실패해요. 이 테이블들의 보존 기간을 예상되는 가장 긴 갱신 간격보다 길게 설정하세요. - 커스텀 증분 동적 테이블은 기본 키를 자동으로 파생하지 않아요. 다운스트림 동적 테이블이나 스트림이 변경을 효율적으로 소비하려면 수동으로
RELY기본 키 제약 조건을 추가하세요. MERGE작업은 표준 비결정성 규칙의 적용을 받아요. 여러 소스 행이 같은 target 행과 일치하면 결과가 비결정적이 돼요. 결정적 결과를 보장하려면 중복 제거(이를테면ROW_NUMBER()와QUALIFY)를 추가하세요.CHANGES()절 밖의 객체(예: 조인의 차원 테이블)는 증분이 아니라 갱신 스냅샷 시점의 상태로 읽혀요.CHANGES(INFORMATION => APPEND_ONLY)절은 커스텀 증분 동적 테이블에 네이티브 append-only 의미론을 부여해요. SELECT 기반 정의를 사용하는 동적 테이블은 이를 직접 지원하지 않아요.
RELY 기본 키와 CHANGES() 동작
CHANGES(INFORMATION=>DEFAULT)에서 Snowflake가 CHANGES() 출력에서 행을 식별하는 방식은 기본 테이블에 RELY 기본 키가 있는지에 따라 달라져요:
| 기본 테이블 구성 | 행 신원 | CHANGES()에 미치는 영향 |
|---|---|---|
RELY 기본 키 없음 |
내부 행 추적 | 같은 값을 가진 DELETE 후 INSERT가 두 개의 별도 변경 행으로 나타남 |
RELY 기본 키 있음 |
기본 키 값 | 같은 키 값의 DELETE 후 INSERT가 서로 상쇄됨(순 변화 없음) |
같은 행의 delete-then-insert가 병합 로직에 보이지 않아야 한다면(예: INSERT OVERWRITE에서 비롯됨), Snowflake가 이 쌍을 CHANGES()에 도달하기 전에 압축할 수 있도록 기본 키 제약 조건에 RELY를 추가하세요.
커스텀 증분 동적 테이블에서 CHANGES(INFORMATION=>APPEND_ONLY)는 행 신원에 기본 키를 사용하지 않아요.
제한 사항
- dbt 통합 없음.:
CREATE OR ALTER또는 DCM Projects를 사용해 커스텀 증분 동적 테이블 정의를 업데이트하세요. REFRESH USING정의와 속성(예:TARGET_LAG)을 수정할 수 있는 것은CREATE OR ALTER뿐이에요.- 업스트림 스키마 변경은 다음 갱신이 컴파일 오류로 실패하게 해요. 업스트림 객체가 변경됐다면(교체·삭제가 아닌) 업데이트된
REFRESH USING정의로CREATE OR ALTER를 사용해 복구하세요. 다음 갱신은 마지막 성공 갱신에서 이어져요. 업스트림이CREATE OR REPLACE를 사용했다면 변경 추적이 깨지므로CREATE OR REPLACE로 다운스트림을 다시 만들어야 해요. - Frozen regions(
FROZEN WHERE)과INSERT ONLY INPUTS는REFRESH USING과 결합할 수 없어요. - 클론: 커스텀 증분 동적 테이블의 기본 테이블이 갱신된 후 교체되면 클론된 테이블은 갱신되지 않아요. 먼저 클론 소스를 갱신한 다음 클론하세요(필요하면 수동으로).
- 복제에는 다음 제한 사항이 있어요: 서로 다른 failover 그룹에 있는 커스텀 증분 동적 테이블과 그 기본 테이블은 지원되지 않아요. Apache Iceberg™ 기본 테이블이 있는 커스텀 증분 동적 테이블, 또는 그 자체가 동적 Iceberg 테이블인 커스텀 증분 동적 테이블의 복제는 지원되지 않아요. 커스텀 증분 동적 테이블의 기본 테이블이 교체되거나 다시 생성되면 복제본은 최대 두 번의 복제 갱신 주기 후에만 사용할 수 있어요.
지원되는 데이터 타입
커스텀 증분 동적 테이블은 컬럼 목록과 REFRESH USING DML에서 다음을 제외한 모든 Snowflake SQL 데이터 타입을 지원해요:
- 구조화된 데이터 타입(구조화된 OBJECT, 구조화된 ARRAY, MAP). 반구조화 타입(정의된 스키마가 없는 VARIANT, OBJECT, ARRAY)은 완전히 지원돼요.
INTERVAL타입.MERGE ON절의 지리 공간 타입.GEOGRAPHY와GEOMETRY는 투영 컬럼으로 지원되지만,MERGE INTO SELF ... ON에는 사용할 수 없어요. 직접 동등성(예:tgt.geo_col = source.geo_col)과ST_EQUALS같은 지리 공간 함수는REFRESH USING에서 지원되지 않아요. 대신 비지리 공간 조인 키(예: 기본 키 컬럼)를 사용하세요.
사용자 정의 함수
커스텀 증분 동적 테이블은 다음 제약 조건과 함께 REFRESH USING에서 스칼라 UDF를 지원해요:
- 본문에 서브쿼리가 포함된 SQL UDF는 지원되지 않아요.
CHANGES()절의 쿼리에 대해 Python, Java, JavaScript, Scala 같은 비-SQL 스칼라 UDF는IMMUTABLE로 표시해야 해요.- 테이블 함수(UDTF)는
REFRESH USING에서 지원되지 않아요.
함수가 같은 입력에 대해 항상 같은 출력을 반환할 때만 IMMUTABLE로 표시하세요. Snowflake는 이를 검증하지 않으며, 잘못 사용하면 잘못된 결과가 나올 수 있어요.
동적 테이블이나 업스트림 뷰가 의존하는 IMMUTABLE UDF를 교체하면 이후 갱신이 깨질 수 있어요. UDF를 교체한 후 동적 테이블을 다시 만드세요.
Iceberg 기본 테이블과 CHANGES()
기본 테이블이 Iceberg 테이블일 때 CHANGES() 지원을 다음 표가 요약해요.
| 적용 상황 | CHANGES(INFORMATION => DEFAULT) |
CHANGES(INFORMATION => APPEND_ONLY) |
|---|---|---|
| Snowflake 관리 Iceberg V2 기본 테이블 | 지원. 마지막 갱신 이후 외부 엔진이 테이블에 썼다면: 지원. 변경되지 않은 행이 delete+insert 쌍으로 나타남(copy-on-write). | 지원. 마지막 갱신 이후 외부 엔진이 테이블에 썼다면: 지원되지 않음(갱신 실패). |
| Snowflake 관리 Iceberg V3 기본 테이블 | 지원. | 지원. 마지막 갱신 이후 외부 엔진이 테이블에 썼다면: 지원되지 않음(갱신 실패). |
| 외부 카탈로그가 있는 Iceberg V2 기본 테이블 | 소스 기본 키에 RELY가 있을 때만 지원. |
지원되지 않음. |
| 외부 카탈로그가 있는 Iceberg V3 기본 테이블 | 지원. | 지원되지 않음. |
Snowflake 관리 Iceberg 테이블 위의 스트림에서 같은 외부 엔진 쓰기 동작에 대해서는 스트림 제한 사항 문서를 참조하세요.
다음 단계
- 디자인 패턴: 전체 CDC MERGE와 SELF 읽기 패턴을 포함한 더 많은 예시.
- 스트림·태스크에서 마이그레이션: 기존 스트림-태스크 파이프라인을 동적 테이블로 이동.
- 스트림과 변경 추적: Snowflake에서 변경 추적이 작동하는 방식.
- CREATE DYNAMIC TABLE: 전체 SQL 참조.