동적 테이블용 입력 데이터 최적화
동적 테이블용 입력 데이터 최적화
이 페이지는 기본 키(primary keys)와 클러스터링이 Snowflake가 각 갱신 중 더 적은 행을 처리하도록 돕는 방법을 설명해요. 파이프라인 개발자는 INSERT OVERWRITE 또는 다른 전체 교체(full-replacement) 로딩 패턴을 사용하는 기본 테이블 위에 파이프라인을 구축하기 전에 이 페이지를 읽어야 해요.
기본 테이블에 RELY 속성이 있는 기본 키를 추가하세요. 이렇게 하면 Snowflake가 INSERT OVERWRITE 후에 실제로 변경된 행을 감지할 수 있고, 모든 행을 새 것으로 취급하지 않아요:
ALTER TABLE dim_customers
ADD CONSTRAINT pk_dim_customers PRIMARY KEY (customer_id) RELY;
출처: Snowflake 문서
본문
기본 키가 갱신 작업을 줄이는 방법
각 갱신 중 Snowflake는 기본 테이블에서 어떤 행이 변경됐는지 결정해야 해요. 기본 키가 없으면 INSERT OVERWRITE는 모든 변경 추적 메타데이터를 교체하고 Snowflake는 모든 행을 새 것으로 취급해요. 이렇게 하면 실제로 변경된 데이터가 작은 비율일 때도 전체 갱신이 강제돼요.
기본 테이블에 RELY 속성이 있는 PRIMARY KEY 제약 조건이 있으면 Snowflake는 그 키 값을 업데이트 전반의 안정적인 행 식별자로 사용해요. 덮어쓰기 전후의 기본 키 값을 비교하고, 실제로 변경된 행만 식별해 다운스트림 동적 테이블로 처리해요.
RELY는 고유성을 강제하지 않음
RELY는 미래의 중복을 방지하지 않아요.RELY를 설정하기 전에 업스트림 ETL에서 고유성을 검증하고, 매 로드 주기 후 검사를 반복하세요.
SELECT customer_id, COUNT(*) AS cnt
FROM dim_customers
GROUP BY customer_id
HAVING cnt > 1;
+-------------+-----+
| CUSTOMER_ID | CNT |
|-------------+-----|
-- (no rows = safe to use RELY)
동적 테이블용 기본 키 유형
Snowflake는 변경 추적에 대해 두 가지 종류의 기본 키를 인식해요. 다음 표는 각각이 언제 적용되는지 요약해요.
| 유형 | 생성 방법 | 사용 시기 |
|---|---|---|
RELY가 있는 기본 테이블 PRIMARY KEY |
기본 테이블에 제약 조건을 선언하고 RELY를 설정. |
INSERT OVERWRITE, COPY INTO, 또는 외부 ETL로 로드되는 기본 테이블. |
| 시스템 파생 고유 키 | Snowflake가 동적 테이블의 정의(GROUP BY 또는 QUALIFY ROW_NUMBER() = 1)에서 추론. |
구성상 키당 한 행을 만들어내는 동적 테이블. |
RELY가 있는 기본 테이블 기본 키
기본 테이블에 RELY 속성이 있는 PRIMARY KEY 제약 조건이 있으면 Snowflake는 모든 다운스트림 동적 테이블의 행 수준 변경 추적에 그 키를 사용해요. 이것이 INSERT OVERWRITE 워크로드를 최적화하는 주요 메커니즘이에요.
CREATE OR REPLACE TABLE dim_products (
product_id INT PRIMARY KEY RELY,
product_name VARCHAR,
category VARCHAR,
price DECIMAL(10,2)
);
시스템 파생 고유 키
Snowflake는 동적 테이블의 정의에서 자동으로 고유 키를 파생할 수 있어요. 다음 구조물이 시스템 파생 고유 키를 만들어요:
GROUP BY: 그룹화 컬럼이 키를 형성하는데, 각 그룹이 정확히 하나의 출력 행을 생성하기 때문이에요.QUALIFY ROW_NUMBER() = 1: partition-by 컬럼이 키를 형성하는데, 필터가 파티션당 정확히 하나의 행을 유지하기 때문이에요.- 기본 테이블 기본 키 통과(passthrough): 동적 테이블 정의가 함수, 캐스트, 표현식을 적용하지 않고
RELY기본 키 컬럼을 통과시키면 시스템 파생 고유 키가 동적 테이블로 전파돼요.
동적 테이블에 시스템 파생 고유 키가 있는지 확인하려면 SHOW UNIQUE KEYS를 실행하세요:
SHOW UNIQUE KEYS IN dt_orders;
+----------------------------+-----------+--------+-------------+-----------+-----+----------------+
| CREATED_ON | TABLE_NAME| SCHEMA | COLUMN_NAME | KEY_INDEX | ... | CONSTRAINT_TYPE|
|----------------------------+-----------+--------+-------------+-----------+-----+----------------|
| 2025-01-15 08:32:28 +0000 | DT_ORDERS| TUTORIAL| ORDER_ID | 1 | ... | UNIQUE |
+----------------------------+-----------+--------+-------------+-----------+-----+----------------+
쿼리가 행을 반환하지 않으면 동적 테이블에 시스템 파생 고유 키가 없는 거예요.
다음 조건 중 하나 이상이 충족되면 다운스트림 동적 테이블이 전체 갱신 업스트림에 대해 증분 갱신을 사용할 수 있어요:
- 업스트림에 시스템 파생 고유 키가 있음.
- 업스트림에 frozen region이 있음.
그렇지 않으면 REFRESH_MODE=INCREMENTAL로 다운스트림 테이블을 만드는 것이 실패하고, REFRESH_MODE=AUTO는 FULL로 결정돼요.
INSERT OVERWRITE 워크로드 최적화
INSERT OVERWRITE는 기본 키의 혜택을 받는 가장 흔한 로딩 패턴이에요. 기본 키가 없으면 모든 INSERT OVERWRITE는 몇 행만 변경됐어도 Snowflake에 전체 삭제·재삽입처럼 보여요.
다음 예시는 권장 패턴을 보여줘요. 기본 테이블이 RELY가 있는 PRIMARY KEY를 선언하고, 다운스트림 동적 테이블은 증분 갱신을 사용해요:
CREATE OR REPLACE TABLE dim_customers (
customer_id INT PRIMARY KEY RELY,
customer_name VARCHAR,
region VARCHAR,
segment VARCHAR
);
-- 외부 ETL이 이 테이블을 매일 밤 INSERT OVERWRITE로 다시 씁니다.
-- Snowflake가 기본 키를 사용해 실제로 변경된 행을 감지합니다.
CREATE OR REPLACE DYNAMIC TABLE dt_customer_orders
TARGET_LAG = '10 minutes'
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT
o.order_id,
o.customer_id,
c.customer_name,
c.region,
...
-- remaining columns omitted for brevity
FROM raw_orders o
JOIN dim_customers c ON o.customer_id = c.customer_id;
외부 ETL 프로세스가 dim_customers에 INSERT OVERWRITE를 실행하면 Snowflake는 덮어쓰기 전후의 기본 키 값을 비교하고, 변경된 차원 행만 식별하고, 그 변경된 차원에 조인하는 팩트 행만 갱신해요.
📌 기본 테이블에 기본 키를 추가하는 것이 불가능하다면 대안으로
ADAPTIVE갱신을 고려하세요.ADAPTIVE는 기본 테이블에 기본 키가 없어도INSERT OVERWRITE워크로드를 처리할 수 있어요.
기본 테이블 교체 방법이 기본 키에 미치는 영향
모든 테이블 교체 패턴이 기본 키 제약 조건과 변경 추적 기록을 보존하는 것은 아니에요.
| 방법 | 기본 키 보존? | 변경 기록 보존? | 원자적? | 다운스트림 동적 테이블에 미치는 영향 |
|---|---|---|---|---|
INSERT … OVERWRITE (권장) |
예 | 예 | 예 | 변경된 행만 처리됨. |
TRUNCATE + INSERT/COPY INTO |
예 | 예 | 아니요 (두 문 사이 테이블이 비어 있음). | 로드가 완료되면 INSERT OVERWRITE와 동일하지만, 로드 중 실행되는 갱신은 빈 테이블을 봄. |
CREATE OR REPLACE TABLE |
아니요 (다시 선언하지 않으면 기본 키가 삭제됨). | 아니요 (모든 스트림이 지연됨). | 예 | 전체 재초기화 필요. 기본 키를 다시 선언해도 초기 갱신에 비교할 이전 상태가 없음. |
동적 테이블 파이프라인을 공급하는 기본 테이블에는 INSERT OVERWRITE를 사용하세요. 이미 다운스트림 동적 테이블이 있는 테이블에는 CREATE OR REPLACE를 피하세요.
전체 갱신의 다운스트림에서 증분 갱신 활성화
일반적으로 REFRESH_MODE=INCREMENTAL 동적 테이블은 REFRESH_MODE=FULL 동적 테이블에서 읽을 수 없어요. 전체 갱신이 변경 추적 메타데이터를 폐기하기 때문이에요. 업스트림 전체 갱신 동적 테이블에 시스템 파생 고유 키가 있으면 Snowflake가 전체 갱신 사이의 차이를 계산할 수 있고, 다운스트림 테이블은 증분으로 갱신할 수 있어요.
REFRESH_MODE = INCREMENTAL을 명시적으로 설정 시스템 파생 고유 키가 있는 전체 갱신 동적 테이블의 다운스트림에서 증분 갱신을 사용하려면 다운스트림 테이블에
REFRESH_MODE=INCREMENTAL을 명시적으로 설정해야 해요. 이 시나리오에서REFRESH_MODE=AUTO를 설정하면 여전히FULL로 결정돼요.
예시: GROUP BY가 시스템 파생 고유 키를 생성
-- Full refresh: GROUP BY가 (order_day, region, segment)에 시스템 파생 고유 키를 생성합니다.
CREATE OR REPLACE DYNAMIC TABLE dt_orders_daily
TARGET_LAG = '30 minutes'
WAREHOUSE = transform_wh
REFRESH_MODE = FULL
AS
SELECT
DATE_TRUNC('day', s.order_date) AS order_day,
c.region,
c.segment,
COUNT(*) AS order_count,
SUM(s.line_total) AS daily_revenue
FROM dt_orders s
JOIN dim_customers c ON s.customer_id = c.customer_id
GROUP BY ALL;
-- Downstream incremental: dt_orders_daily가 시스템 파생 고유 키를 가지므로 작동합니다.
CREATE OR REPLACE DYNAMIC TABLE dt_region_trends
TARGET_LAG = '1 hour'
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT
region,
AVG(daily_revenue) AS avg_daily_revenue,
COUNT(*) AS days_with_orders
FROM dt_orders_daily
GROUP BY region;
시스템 파생 고유 키 확인
다운스트림 증분 동적 테이블을 만들기 전에 업스트림 테이블에 시스템 파생 고유 키가 있는지 확인하세요:
SHOW UNIQUE KEYS IN dt_orders_daily;
+----------------------------+------------------+--------+-------------+-----------+-----+----------------+
| CREATED_ON | TABLE_NAME | SCHEMA | COLUMN_NAME | KEY_INDEX | ... | CONSTRAINT_TYPE|
|----------------------------+------------------+--------+-------------+-----------+-----+----------------|
| 2025-01-15 10:00:00 +0000 | DT_ORDERS_DAILY | TUTORIAL| ORDER_DAY | 1 | ... | UNIQUE |
| 2025-01-15 10:00:00 +0000 | DT_ORDERS_DAILY | TUTORIAL| REGION | 2 | ... | UNIQUE |
| 2025-01-15 10:00:00 +0000 | DT_ORDERS_DAILY | TUTORIAL| SEGMENT | 3 | ... | UNIQUE |
+----------------------------+------------------+--------+-------------+-----------+-----+----------------+
SHOW UNIQUE KEYS가 행을 반환하지 않으면 REFRESH_MODE=INCREMENTAL로 다운스트림 생성이 실패해요:
-- This fails if the upstream table has no system-derived unique key.
CREATE OR REPLACE DYNAMIC TABLE dt_downstream_agg
TARGET_LAG = '1 hour'
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT region, SUM(daily_revenue) AS total_revenue
FROM dt_orders_daily
GROUP BY region;
SQL compilation error:
Incremental refresh mode is not supported because the upstream dynamic table
does not have a unique key for change tracking.
기본 키를 클러스터링과 짝짓기
기본 키는 Snowflake에 어떤 행이 변경됐는지 알려줘요. 클러스터링은 Snowflake가 그 행들을 얼마나 효율적으로 찾을 수 있는지를 결정해요. 기본 테이블이 기본 키 컬럼에 또는 근처에 클러스터링되면 Snowflake는 갱신 중 더 많은 마이크로 파티션을 가지치기(prune)하고 더 적은 데이터를 읽을 수 있어요.
최상의 결과를 위해:
- 기본 키 컬럼 또는 다운스트림 동적 테이블과의 조인 조건에서 가장 자주 사용되는 컬럼에 기본 테이블을 클러스터링하세요.
- 기본 테이블에 쿼리 성능용 클러스터링 키가 이미 있으면 그것이 기본 키와 겹치는지 확인하세요. 겹치는 키는 쿼리 가지치기와 갱신 가지치기 모두에 이점을 줘요.
- 백그라운드 재클러스터링은 새 마이크로 파티션을 만들어 갱신 기간의 일시적 급증을 유발할 수 있다는 점에 유의하세요. 이 급증은 일시적이며 재클러스터링이 완료된 후 해결돼요.
일반적인 클러스터링 지침은 마이크로 파티션과 데이터 클러스터링 문서를 참조하세요.
기본 키 추적 저하 감지
기본 키 기반 변경 추적은 오류를 생성하지 않고 저하되거나 작동을 멈출 수 있어요. Snowflake는 이런 경우 예외를 발생시키지 않아요. 대신 표준 변경 추적 컬럼으로 폴백하는데, 이는 기본 키 기반 추적의 성능 이점을 제거해요.
| 시나리오 | 어떤 일이 일어나는가 | 감지 방법 |
|---|---|---|
| 기본 테이블의 중복 기본 키 값 | 변경 추적이 예상치 못한 결과를 만들 수 있음(행 과소 계산 또는 누락). | RELY를 설정하기 전에 기본 키 컬럼에 GROUP BY/HAVING 쿼리를 실행. |
| 기본 키 컬럼의 마스킹 정책 | Snowflake가 키 값을 읽을 수 없어 표준 변경 추적 컬럼으로 폴백. | 마스킹 정책이 어떤 기본 키 컬럼에 적용되는지 확인. |
ALTER TABLE이 기본 키 제약 조건을 삭제 |
시스템 파생 고유 키가 다운스트림 동적 테이블에서 사라짐. | 스키마 변경 후 SHOW UNIQUE KEYS 실행. |
기본 키 컬럼의 마스킹 정책
마스킹 정책이 기본 키 컬럼을 난독화하면 Snowflake는 그 컬럼을 변경 추적에 사용할 수 없어요. 동적 테이블은 갱신을 계속하지만, Snowflake는 표준 변경 추적 컬럼으로 폴백해요. INSERT OVERWRITE 워크로드에서는 이 폴백이 모든 갱신이 모든 행을 처리하게 만드는데, INSERT OVERWRITE가 표준 추적 컬럼을 재설정하기 때문이에요. 일반 DML 작업(INSERT, UPDATE, DELETE)에서는 표준 변경 추적 컬럼이 여전히 행 수준 변경을 감지할 수 있어요.
이를 감지하려면 마스킹 정책이 기본 키 컬럼에 적용되는지 확인하세요:
-- 기본 키에 사용되는 컬럼의 마스킹 정책 확인
SELECT *
FROM TABLE(INFORMATION_SCHEMA.POLICY_REFERENCES(
REF_ENTITY_NAME => 'dim_customers',
REF_ENTITY_DOMAIN => 'TABLE'
));
마스킹 정책이 기본 키 컬럼에 적용되면 SHOW UNIQUE KEYS 출력과 관계없이 기본 키 기반 변경 추적이 비활성화돼요. 기본 키 컬럼에서 마스킹 정책을 제거하거나 비키(non-key) 컬럼으로 옮기세요.
직접 해보기: 기본 키 유무에 따른 갱신 비교
이 튜토리얼은 같은 데이터와 같은 조인 쿼리로 두 파이프라인을 구축해요. 유일한 차이는 차원 테이블에 기본 키가 있는지 여부예요. 그런 다음 INSERT OVERWRITE로 차원 테이블 재작성을 시뮬레이션하고 갱신 성능을 비교해요.
사전 요구 사항
- 데이터베이스, 스키마, 동적 테이블을 만들 권한이 있는 Snowflake 계정.
- X-Small 웨어하우스. 튜토리얼은 모든 예시에서
transform_wh를 사용해요.
1단계: 소스 데이터 만들기
CREATE DATABASE IF NOT EXISTS mydb;
CREATE SCHEMA IF NOT EXISTS mydb.myschema;
USE SCHEMA mydb.myschema;
-- 기본 키가 있는 차원 테이블
CREATE OR REPLACE TABLE dim_products_with_pk (
product_id INT PRIMARY KEY RELY,
product_name VARCHAR(200),
category VARCHAR(100),
price DECIMAL(10,2)
)
CHANGE_TRACKING = TRUE;
-- 기본 키가 없는 차원 테이블 (나머지 스키마는 동일)
CREATE OR REPLACE TABLE dim_products_no_pk (
product_id INT,
product_name VARCHAR(200),
category VARCHAR(100),
price DECIMAL(10,2)
)
CHANGE_TRACKING = TRUE;
-- 공유 팩트 테이블
CREATE OR REPLACE TABLE fact_orders (
order_id INT,
product_id INT,
quantity INT,
order_date TIMESTAMP_NTZ
)
CHANGE_TRACKING = TRUE;
두 차원 테이블에 100,000개의 제품을, 팩트 테이블에 1,000만 개의 주문을 삽입하세요:
INSERT INTO dim_products_with_pk (product_id, product_name, category, price)
SELECT
SEQ4() + 1 AS product_id,
'Product ' || LPAD(TO_VARCHAR(SEQ4() + 1), 6, '0') AS product_name,
'Category ' || LPAD(TO_VARCHAR(MOD(SEQ4(), 10) + 1), 2, '0') AS category,
ROUND(5.00 + MOD(SEQ4(), 500) * 0.50, 2) AS price
FROM TABLE(GENERATOR(ROWCOUNT => 100000));
INSERT INTO dim_products_no_pk
SELECT * FROM dim_products_with_pk;
INSERT INTO fact_orders (order_id, product_id, quantity, order_date)
SELECT
SEQ4() + 1 AS order_id,
MOD(SEQ4(), 100000) + 1 AS product_id,
MOD(SEQ4(), 10) + 1 AS quantity,
DATEADD(SECOND, SEQ4(), '2025-01-01 00:00:00') AS order_date
FROM TABLE(GENERATOR(ROWCOUNT => 10000000));
2단계: 두 파이프라인 만들고 초기 갱신 실행
기본 키 차원 테이블로 하나, 없는 것으로 하나의 파이프라인을 만들어요. 둘 다 같은 조인 쿼리를 사용해요.
-- Pipeline WITH primary key
CREATE OR REPLACE DYNAMIC TABLE dt_enriched_with_pk
TARGET_LAG = DOWNSTREAM
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT
f.order_id,
f.product_id,
d.product_name,
d.category,
f.quantity,
d.price,
f.quantity * d.price AS order_total,
f.order_date
FROM fact_orders f
JOIN dim_products_with_pk d ON f.product_id = d.product_id;
-- Pipeline WITHOUT primary key
CREATE OR REPLACE DYNAMIC TABLE dt_enriched_no_pk
TARGET_LAG = DOWNSTREAM
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT
f.order_id,
f.product_id,
d.product_name,
d.category,
f.quantity,
d.price,
f.quantity * d.price AS order_total,
f.order_date
FROM fact_orders f
JOIN dim_products_no_pk d ON f.product_id = d.product_id;
-- Run the initial refresh for both
ALTER DYNAMIC TABLE dt_enriched_with_pk REFRESH;
ALTER DYNAMIC TABLE dt_enriched_no_pk REFRESH;
3단계: INSERT OVERWRITE 시뮬레이션 및 비교
제품의 10%(Category 01)에 대한 가격을 바꾸고 나머지 행은 동일하게 유지하면서 두 차원 테이블을 다시 써요:
INSERT OVERWRITE INTO dim_products_with_pk
SELECT
product_id,
product_name,
category,
CASE WHEN category = 'Category 01' THEN ROUND(price * 1.10, 2) ELSE price END AS price
FROM dim_products_with_pk;
INSERT OVERWRITE INTO dim_products_no_pk
SELECT
product_id,
product_name,
category,
CASE WHEN category = 'Category 01' THEN ROUND(price * 1.10, 2) ELSE price END AS price
FROM dim_products_no_pk;
두 파이프라인을 갱신하고 성능을 비교하세요:
ALTER DYNAMIC TABLE dt_enriched_no_pk REFRESH;
ALTER DYNAMIC TABLE dt_enriched_with_pk REFRESH;
각각의 갱신 기록을 확인하세요:
SELECT
name,
state,
refresh_trigger,
DATEDIFF('second', refresh_start_time, refresh_end_time) AS duration_seconds,
statistics:numInsertedRows::INT AS num_inserted_rows,
statistics:numDeletedRows::INT AS num_deleted_rows
FROM
TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_REFRESH_HISTORY(
NAME_PREFIX => 'MYDB.MYSCHEMA.DT_ENRICHED_',
ERROR_ONLY => FALSE
))
ORDER BY refresh_start_time DESC
LIMIT 4;
+-----------------------+-----------+-----------------+------------------+-------------------+------------------+
| NAME | STATE | REFRESH_TRIGGER | DURATION_SECONDS | NUM_INSERTED_ROWS | NUM_DELETED_ROWS |
|-----------------------+-----------+-----------------+------------------+-------------------+------------------|
| DT_ENRICHED_WITH_PK | SUCCEEDED | MANUAL | 12 | 1000000 | 1000000 |
| DT_ENRICHED_NO_PK | SUCCEEDED | MANUAL | 34 | 10000000 | 10000000 |
| DT_ENRICHED_WITH_PK | SUCCEEDED | MANUAL | 45 | 10000000 | 0 |
| DT_ENRICHED_NO_PK | SUCCEEDED | MANUAL | 46 | 10000000 | 0 |
+-----------------------+-----------+-----------------+------------------+-------------------+------------------+
기본 키가 있는 파이프라인은 변경된 차원 행의 10%를 참조하는 100만 팩트 행만 처리했어요. 기본 키가 없는 파이프라인은 INSERT OVERWRITE 후 Snowflake가 변경된 행과 변경되지 않은 행을 구분할 수 없었으므로 1,000만 행 전체를 갱신했어요.
💡 팩트 테이블이 커지고 변경된 차원 행의 비율이 줄어들수록 성능 격차는 커져요. 수백만 차원 행과 로드 주기당 한 자리 수 비율 변경이 있는 프로덕션 파이프라인에서는 기본 키 기반 변경 추적이 갱신 시간을 한 자릿수로 줄일 수 있어요.
정리
DROP DATABASE mydb;
다음 단계
- 증분 갱신의 쿼리 수준 튜닝은 증분 갱신용 쿼리 최적화 문서를 참조하세요.
- 변경되지 않은 과거 파티션을 건너뛰려면 Frozen regions와 backfill 문서를 참조하세요.
- 갱신 기간을 추적하고 회귀를 감지하려면 동적 테이블 모니터링 문서를 참조하세요.
- 갱신 모드의 크레딧 영향을 이해하려면 동적 테이블 비용 이해 문서를 참조하세요.
- 동적 테이블 빠른 시작 모범 사례.