동적 테이블 디자인 패턴
동적 테이블 디자인 패턴
이 페이지는 동적 테이블 파이프라인을 구조화하는 일반적인 레시피를 모아둔 거예요. 아래 의사결정 표를 사용해 올바른 시작점을 찾으세요. 테이블별 구성(target lag, 갱신 모드, 웨어하우스 선택)에 대해서는 동적 테이블 빠른 시작 모범 사례 문서를 참조하세요.
출처: Snowflake 문서
본문
패턴 선택
| 패턴 | 사용 시기 | 작동 방식 |
|---|---|---|
| 다중 테이블 파이프라인 | 정의에 논리적 단계가 둘 이상일 때 | 중간 테이블에 TARGET_LAG = DOWNSTREAM; 메달리온 레이어 |
| 컨트롤러 테이블 | 독립 파이프라인 간 조정된 갱신이 필요할 때 | 독립 리프 lag이 있는 제로 행 조정 테이블 |
| SCD Type 1 중복 제거 | 소스가 CDC 레코드를 추가하고 현재 상태가 필요할 때 | QUALIFY ROW_NUMBER; 순서 어긋난 도착 처리 |
| 스트림-정적 조인 | 차원 조회와 삭제 전파가 있는 CDC 강화 | CHANGES와 LEFT OUTER JOIN이 있는 MERGE INTO SELF |
| Top-K 리더보드 | 축소되거나 성장하는 경계 결과 집합을 유지 | FROM SELF로 현재 상태를 읽는 MERGE INTO SELF |
| 일일 스냅샷 | CHANGES 없이 주기적 스냅샷 누적 |
ROW_TIMESTAMP가 있는 INSERT INTO SELF |
정의를 다중 테이블 파이프라인으로 분해
모놀리식 정의를 각각 하나의 논리적 단계를 처리하는 둘 이상의 동적 테이블 파이프라인으로 나누세요. 많은 팀이 메달리온 용어(bronze, silver, gold)로 파이프라인 레이어를 구성해요. 동적 테이블 용어로: bronze는 원시 랜딩 테이블, silver는 TARGET_LAG = DOWNSTREAM이 있는 정리·표준화된 동적 테이블, gold는 시간 기반 신선도 목표가 있는 집계 동적 테이블이에요.
조인과 집계를 여러 동적 테이블에 분해하면 각 단계가 증분으로 갱신될 수 있어요. 조인을 첫 번째 동적 테이블에, 집계를 다음에 배치하세요.
⚠️ 하나의 동적 테이블에 조인, 집계, 윈도우 함수를 결합하는 것을 피하세요. 이렇게 하면 전체 계산이 매 갱신마다 다시 실행돼요. 이 연산자들을 서로 다른 파이프라인 단계로 분리하면 각 단계에 증분 갱신이 가능해요.
CREATE OR REPLACE DYNAMIC TABLE dt_orders_enriched
TARGET_LAG = DOWNSTREAM
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT
o.order_id,
o.order_date,
o.quantity,
o.unit_price,
o.quantity * o.unit_price AS line_total,
c.region,
c.segment,
...
FROM raw_orders o
JOIN dim_customers c ON o.customer_id = c.customer_id;
CREATE OR REPLACE DYNAMIC TABLE dt_orders_daily
TARGET_LAG = '30 minutes'
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT
DATE_TRUNC('day', order_date) AS order_day,
region,
segment,
COUNT(*) AS order_count,
SUM(line_total) AS daily_revenue
FROM dt_orders_enriched
GROUP BY ALL;
silver 테이블(dt_orders_enriched)은 TARGET_LAG = DOWNSTREAM을 사용해 gold 테이블이 신선한 데이터를 필요로 할 때만 갱신돼요. gold 테이블(dt_orders_daily)이 시간 기반 신선도 목표를 소유하고 전체 파이프라인을 이끌어요. 일반적인 변형으로, 시간별 데이터를 일일 합계로 롤업하는 추가 집계 레이어인 두 번째 gold 테이블을 추가해요.
컨트롤러 테이블로 여러 파이프라인 동기화
컨트롤러 동적 테이블은 독립 파이프라인을 단일 조정된 갱신으로 병합해요. 트리거되면 컨트롤러가 같은 타임스탬프에서 모든 입력을 읽어요. 컨트롤러 갱신 사이에는 리프 테이블이 자신의 스케줄로 갱신돼요. 시간적으로 일관적이어야 하는 독립 도메인의 데이터를 대시보드나 다운스트림 소비자가 조인할 때 이 패턴을 사용하세요.
컨트롤러는 여러 리프 테이블에 의존하는 제로 행(0행) 동적 테이블이에요. 리프는 자신의 시간 기반 target lag을 가지며 컨트롤러 갱신 사이에 독립적으로 갱신돼요.
CREATE OR REPLACE DYNAMIC TABLE dt_orders
TARGET_LAG = '5 minutes'
WAREHOUSE = transform_wh
AS
SELECT order_id, customer_id, order_date, line_total
FROM raw_orders;
CREATE OR REPLACE DYNAMIC TABLE dt_returns
TARGET_LAG = '5 minutes'
WAREHOUSE = transform_wh
AS
SELECT return_id, order_id, return_date, refund_amount
FROM raw_returns;
CREATE OR REPLACE DYNAMIC TABLE dt_checkpoint
TARGET_LAG = DOWNSTREAM
WAREHOUSE = transform_wh
AS
SELECT 1 AS sync_flag FROM dt_orders, dt_returns LIMIT 0;
컨트롤러가 깨져도 리프는 자신의 스케줄로 독립적으로 갱신을 계속해요. 하나의 리프 동적 테이블이 실패하면 컨트롤러도 실패해요. 컨트롤러만이 아니라 컨트롤러 그룹의 모든 테이블을 모니터링하세요.
⚠️
FROM절에 너무 많은 리프 테이블을 나열하는 것을 피하세요. 컨트롤러의 교차 조인은 실행되지 않지만(LIMIT 0때문), 컴파일 시간이 테이블 수에 따라 늘어나요. 리프가 5~6개보다 많으면 중간 컨트롤러로 리프를 그룹화하는 것을 고려하세요.
SCD Type 1로 최신 행 중복 제거
기본 테이블이 append-only 변경 데이터 캡처(CDC) 레코드를 받으면 QUALIFY ROW_NUMBER를 사용해 비즈니스 키별 최신 행만 유지하세요. 윈도우 함수는 수집 순서와 관계없이 올바른 행을 선택해, 추가 로직 없이 순서가 어긋난 도착을 처리해요.
CREATE OR REPLACE DYNAMIC TABLE dt_customers_current
TARGET_LAG = '10 minutes'
WAREHOUSE = transform_wh
REFRESH_MODE = INCREMENTAL
AS
SELECT * EXCLUDE (raw_metadata_col)
FROM raw_customers_cdc
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) = 1;
SELECT * EXCLUDE를 사용하면 증분 스키마 진화를 지원해요: 기본 테이블에 추가되거나 제거된 컬럼이 동적 테이블 정의를 바꾸지 않고 자동으로 전파돼요. 스키마 진화에 대한 자세한 내용은 동적 테이블 수정 문서를 참조하세요.
소프트 삭제를 처리하려면 QUALIFY 표현식에 필터를 추가하세요:
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) = 1
AND NOT is_deleted
이렇게 하면 추가 오케스트레이션 없이 소프트 삭제 로직을 단일 윈도우 표현식으로 표현해요.
최상의 증분 성능을 위해 단조(monotonic) ORDER BY 컬럼(증가만 하는 시퀀스 번호나 타임스탬프)을 사용하세요. QUALIFY 표현식을 동적 테이블의 최상위 구조물로 유지하세요. 추가 변환이 필요하면 같은 정의에서 결합하지 말고 다운스트림 동적 테이블에 배치하세요.
💡 행이 마지막으로 갱신된 시점을 추적하는 내장 메커니즘으로
ROW_TIMESTAMP를 사용하세요. 정의에 컬럼으로CURRENT_TIMESTAMP()를 추가하는 것보다 깔끔하며, 이는 전체 갱신 모드를 강제해요.
전체 CDC가 있는 스트림-정적 조인 (MERGE)
스트림-정적 조인은 마지막 갱신 이후 변경된 행만(CHANGES()를 사용하는 소스에서) 읽고 조회 값을 제공하는 차원 테이블과 조인해요. 차원 테이블은 각 갱신에서 전체로 읽히지만 어떤 행이 처리되는지는 좌우하지 않아요.
append-only 변형(INSERT INTO SELF)에 대해서는 append-only 강화 문서를 참조하세요.
CREATE OR REPLACE DYNAMIC TABLE dt_enriched_inventory (
sku_id INT,
product_name STRING,
category STRING,
warehouse_name STRING,
region STRING,
qty_on_hand INT
)
TARGET_LAG = '30 minutes'
WAREHOUSE = transform_wh
REFRESH USING (
MERGE INTO SELF AS tgt
USING (
SELECT
s.sku_id,
p.product_name,
p.category,
w.warehouse_name,
w.region,
s.qty_on_hand,
s.METADATA$ACTION AS action,
ROW_NUMBER() OVER (
PARTITION BY s.sku_id
ORDER BY s.event_timestamp DESC,
CASE s.METADATA$ACTION WHEN 'INSERT' THEN 0 ELSE 1 END
) AS rn
FROM stock CHANGES(INFORMATION => DEFAULT) AS s
LEFT OUTER JOIN products AS p ON s.product_id = p.product_id
LEFT OUTER JOIN warehouses AS w ON s.warehouse_id = w.warehouse_id
QUALIFY rn = 1
) AS src
ON tgt.sku_id = src.sku_id
WHEN MATCHED AND src.action = 'DELETE' THEN DELETE
WHEN MATCHED AND src.action = 'INSERT' THEN
UPDATE SET
tgt.product_name = src.product_name,
tgt.category = src.category,
tgt.warehouse_name = src.warehouse_name,
tgt.region = src.region,
tgt.qty_on_hand = src.qty_on_hand
WHEN NOT MATCHED AND src.action = 'INSERT' THEN
INSERT (sku_id, product_name, category, warehouse_name, region, qty_on_hand)
VALUES (src.sku_id, src.product_name, src.category, src.warehouse_name, src.region, src.qty_on_hand)
);
이 패턴은 병합 전에 stock의 CDC 변경을 차원 조회로 강화해요:
- stock 변경이
LEFT OUTER JOIN을 통해 차원 테이블(products,warehouses)로 강화돼요. ROW_NUMBER중복 제거가MERGE에 키당 하나의 소스 행을 보장해요.- 같은 간격에 키에
DELETE와INSERT가 모두 있으면(업데이트),INSERT가 유지돼요. - 삭제는 전파되고, 삽입은 새 행을 만들고, 업데이트는 기존 행을 수정해요.
Top-K 리더보드 (SELF를 읽는 MERGE)
이 패턴은 커스텀 증분화를 사용해 점수가 변함에 따라 축소되거나 성장하는 경계 top-K 결과 집합을 유지해요.
CREATE OR REPLACE DYNAMIC TABLE dt_leaderboard (
player_id INT,
total_score INT,
rank INT
)
TARGET_LAG = '30 minutes'
WAREHOUSE = transform_wh
REFRESH USING (
MERGE INTO SELF AS tgt
USING (
WITH candidates AS (
SELECT
COALESCE(ch.player_id, cur.player_id) AS player_id,
COALESCE(ch.total_score, cur.total_score) AS total_score
FROM SELF AS cur
FULL OUTER JOIN (
SELECT player_id, total_score
FROM player_scores CHANGES()
WHERE METADATA$ACTION = 'INSERT'
) AS ch ON cur.player_id = ch.player_id
),
ranked AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY total_score DESC, player_id ASC) AS new_rank
FROM candidates
)
SELECT player_id, total_score, new_rank FROM ranked
) AS src
ON tgt.player_id = src.player_id
WHEN MATCHED AND src.new_rank <= 3 THEN
UPDATE SET tgt.total_score = src.total_score, tgt.rank = src.new_rank
WHEN MATCHED AND src.new_rank > 3 THEN
DELETE
WHEN NOT MATCHED AND src.new_rank <= 3 THEN
INSERT (player_id, total_score, rank)
VALUES (src.player_id, src.total_score, src.new_rank)
);
이 패턴은 점수가 변함에 따라 축소되거나 성장하는 경계 top-K 결과 집합을 유지해요:
FROM SELF AS cur는 동적 테이블의 현재 내용을 읽어요.SELF는MERGEtarget이자 읽기 소스가 될 수 있어요.FULL OUTER JOIN은player_scores의 수신 변경과 현재 상태를 병합해요.- 순위 3 아래로 내려간 플레이어는 삭제되고, 새 top-3 플레이어는 삽입돼요.
- 커스텀 증분 동적 테이블은 표준 갱신 모드가 표현할 수 없는 경계 크기 결과 집합을 유지할 수 있고, 이 결과 집합은 다음 증분화 라운드의 메모이제이션된 상태로 재사용될 수 있어요. 위 예시에서 이전 top-K 결과가 현재 변경과 결합되어 모든 과거 데이터를 스캔하는 대신 새 결과를 도출해요.
일일 스냅샷 (CHANGES 절 없음)
모든 커스텀 증분 동적 테이블이 CHANGES()를 필요로 하지는 않아요. 각 갱신에서 소스 데이터를 다시 읽고 ROW_TIMESTAMP를 사용해 시간 경과에 따라 스냅샷을 누적할 수 있어요.
CREATE OR REPLACE DYNAMIC TABLE dt_daily_snapshot (
id INT,
val STRING
)
TARGET_LAG = '1 day'
WAREHOUSE = transform_wh
ROW_TIMESTAMP = TRUE
REFRESH USING (
INSERT INTO SELF
SELECT id, val
FROM src
);
각 갱신은 src의 전체 스냅샷을 동적 테이블에 추가해요. ROW_TIMESTAMP=TRUE는 각 행이 동적 테이블에 삽입된 시점을 자동으로 기록해 시점 상태를 쉽게 쿼리할 수 있게 해요. CHANGES() 절이 없으므로 모든 갱신이 전체 소스를 읽어요. 소스를 작게 유지하거나 필터링을 사용해 비용을 제어하세요.
안티 패턴(Anti-patterns)
이 패턴들은 프로덕션에서 실패나 예상치 못한 동작을 일으켜요.
모든 테이블을 TARGET_LAG = DOWNSTREAM으로 설정
왜 실패하나: 파이프라인의 어떤 동적 테이블에도 시간 기반 target lag이 없으면 아무것도 갱신을 트리거하지 않아요. 파이프라인은 변하지 않는 테이블을 만들고 오류도 발생시키지 않아요.
대신 할 일: 최소한 하나의 동적 테이블(종단 테이블)은 시간 기반 target lag을 가져야 해요. 중간 테이블은 DOWNSTREAM을 사용해요.
-- 틀림: 아무것도 갱신을 트리거하지 않음
CREATE OR REPLACE DYNAMIC TABLE dt_staging
TARGET_LAG = DOWNSTREAM ...;
CREATE OR REPLACE DYNAMIC TABLE dt_summary
TARGET_LAG = DOWNSTREAM ...;
-- 맞음: 종단 테이블이 파이프라인을 이끔다
CREATE OR REPLACE DYNAMIC TABLE dt_summary
TARGET_LAG = '30 minutes' ...;
보존 시간에 비해 짧은 target lag
왜 실패하나: 일시적 오류로 인해 lag이 보존 창을 초과하면 테이블이 지연되고 재초기화가 필요해져요.
대신 할 일: 복구를 위한 버퍼를 남기도록 target lag을 보존 시간보다 몇 배 짧게 설정하세요. 이는 증분과 전체 갱신 모드 모두에 적용돼요.
다음 단계
- 동적 테이블 빠른 시작 모범 사례
- 증분 갱신용 쿼리 최적화
- 동적 테이블의 target lag 설정
- 동적 테이블 수정
- 동적 테이블 비용 이해하기
- 명령형 로직이 필요하거나 이 패턴들에 맞지 않는 워크로드는 동적 테이블 의사결정 가이드 문서를 참조하세요.