데이터 일관성과 파이프라인 경계
데이터 일관성과 파이프라인 경계
동적 테이블 파이프라인은 스냅샷 격리(snapshot isolation)를 제공해요. 모든 갱신이 한 시점에 모든 입력을 읽어요. 이 페이지는 그것이 어떻게 작동하는지, 갱신이 실패하면 어떤 일이 일어나는지, 그리고 갱신 경계(refresh boundaries)로 스케줄링 독립성을 위해 격리를 교환할 때에 대해 다뤄요.
출처: Snowflake 문서
본문
파이프라인에서 스냅샷 격리가 작동하는 방식
한 동적 테이블이 다른 동적 테이블에서 읽으면 Snowflake는 이들을 단일 파이프라인으로 그룹화해요. 그 파이프라인의 모든 갱신은 하나의 조정된 데이터 타임스탬프에서 모든 업스트림 입력을 읽어요. 데이터 타임스탬프는 갱신이 각 입력의 어떤 버전을 읽을지 결정하므로, 다운스트림 결과는 모든 업스트림 테이블의 단일 시점 스냅샷과 일치해요.
이 섹션은 동적 테이블 만들기 문서의 dt_orders와 dt_orders_daily 파이프라인을 사용해요. 이 파이프라인에서 dt_orders_daily는 dt_orders를 dim_customers와 조인해요. 모든 갱신은 같은 타임스탬프에서 둘 다 읽어요. 이게 없으면 갱신이 오늘의 주문과 어제의 고객 속성을 조인해 잘못된 결과를 만들 수 있어요.
스냅샷 격리는 두 가지를 보장해요:
- 각 갱신은 원자적(atomic)이에요. 다운스트림 테이블은 부분적으로 갱신된 업스트림 데이터를 절대 보지 않아요.
- 갱신의 모든 입력은 조인, 필터, 집계에서 사용되든 같은 시점을 반영해요.
스냅샷 격리는 단일 파이프라인의 갱신 주기 내에서 적용돼요. 갱신 밖의 임시 쿼리는 각 테이블의 최신 커밋 버전을 별도로 읽어요.
한 데이터 타임스탬프에서 여러 동적 테이블을 수동으로 갱신하려면 하나의 ALTER DYNAMIC TABLE ... REFRESH 문에 나열하세요. 한 문에서 여러 동적 테이블 갱신 문서를 참조하세요.
실패는 불일치가 아니라 지연을 일으킴
각 갱신은 원자적이에요. 완전히 완료되거나 아무 효과가 없어요. 갱신이 실패하면 모든 다운스트림 테이블은 마지막 성공 버전을 유지해요. 실패 지점을 넘어 진행되지 않아요.
| 시나리오 | 다운스트림 테이블이 보는 것 |
|---|---|
| 업스트림 갱신 실패 | 다운스트림 테이블은 마지막 일관된 버전에 유지. 갱신 상태: UPSTREAM_FAILED. |
| 파이프라인 중간 갱신 실패 | 실패한 것 아래의 모든 동적 테이블에 대해 업스트림 실패와 동일. |
| 연속적인 여러 실패 | 지연(staleness)이 누적. 5회 연속 실패 후 Snowflake가 테이블을 자동으로 일시 중단. 타임아웃과 취소는 이 임계값에 실패로 집계. 업데이트가 시도되지 않았기 때문인 건너뛰어진 갱신만(업스트림이 실패한 경우) 집계되지 않음. |
갱신 기록 쿼리로 파이프라인 상태를 확인할 수 있어요:
SELECT
name,
state,
state_message,
refresh_action,
refresh_trigger
FROM
TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_REFRESH_HISTORY(
NAME => 'mydb.myschema.dt_orders_daily'
))
ORDER BY refresh_start_time DESC
LIMIT 5;
+------------------------------------+-----------------+---------------+----------------+-----------------+
| NAME | STATE | STATE_MESSAGE | REFRESH_ACTION | REFRESH_TRIGGER |
|------------------------------------+-----------------+---------------+----------------+-----------------|
| MYDB.MYSCHEMA.DT_ORDERS_DAILY | UPSTREAM_FAILED | | NO_DATA | SCHEDULED |
| MYDB.MYSCHEMA.DT_ORDERS_DAILY | SUCCEEDED | | INCREMENTAL | SCHEDULED |
| MYDB.MYSCHEMA.DT_ORDERS_DAILY | SUCCEEDED | | INCREMENTAL | SCHEDULED |
| MYDB.MYSCHEMA.DT_ORDERS_DAILY | SUCCEEDED | | INCREMENTAL | SCHEDULED |
| MYDB.MYSCHEMA.DT_ORDERS_DAILY | SUCCEEDED | | INCREMENTAL | CREATION |
+------------------------------------+-----------------+---------------+----------------+-----------------+
UPSTREAM_FAILED 상태는 테이블 자체가 실패한 것이 아니라는 뜻이에요. 업스트림 의존성이 실패했고 이 테이블은 일관성을 보존하기 위해 건너뛰어졌어요. 먼저 업스트림 테이블을 고치면 다운스트림 테이블은 다음 예약 갱신에서 다시 시작돼요.
파이프라인의 한 부분의 지연이 다른 부분을 막아서는 안 될 때 갱신 경계로 분리할 수 있어요.
DYNAMIC_TABLE_REFRESH_BOUNDARY()로 파이프라인 분리
기본적으로 한 동적 테이블을 다른 동적 테이블에서 참조하면 둘 다 같은 조정된 파이프라인에 놓여요. 참조를 DYNAMIC_TABLE_REFRESH_BOUNDARY()로 감싸면 그 결합이 깨져요. 다운스트림 테이블은 감싼 입력을 독립적으로 갱신되는 기본 테이블처럼 취급해요.
CREATE OR REPLACE DYNAMIC TABLE dt_enriched_orders
TARGET_LAG = '5 minutes'
WAREHOUSE = transform_wh
AS
SELECT
s.*,
p.product_category
FROM dt_orders s
JOIN
DYNAMIC_TABLE_REFRESH_BOUNDARY(dt_product_lookup) p
ON s.product_name = p.product_name;
이 예시에서 dt_orders는 직접 의존성(같은 파이프라인, 조정된 갱신, 스냅샷 격리)이에요. dt_product_lookup은 갱신 경계 너머(별도 파이프라인, 독립 스케줄, 그 가장자리에는 스냅샷 격리 없음)에 있어요. dt_enriched_orders의 각 갱신은 그 순간 존재하는 dt_product_lookup의 어떤 버전이든 읽어요.
경계 너머에서 바뀌는 것
| 동작 | 경계 없음 (기본) | 경계 있음 |
|---|---|---|
| 파이프라인 소속 | 같은 파이프라인, 조정된 갱신 | 별도 파이프라인, 독립 스케줄 |
| 스냅샷 격리 | 모든 입력 간 보장 | 경계 입력과 파이프라인 나머지 간 보장되지 않음 |
| 실패 전파 | 업스트림 실패가 다운스트림 차단 | 경계 입력 실패가 다운스트림을 차단하지 않음 |
| Target lag 제약 | 다운스트림 lag은 업스트림보다 크거나 같아야 함 | 경계를 넘어서는 제약 없음 |
경계가 필요한 때
동적 테이블이 다른 동적 테이블을 의존성으로 포함하는 일반 뷰에서 읽을 때는 DYNAMIC_TABLE_REFRESH_BOUNDARY()를 반드시 사용해야 해요. 뷰가 같은 계정에 있든 공유를 통해 접근하든 적용돼요. 동적 테이블이 읽는 최외곽 뷰를 감싸세요.
다음 경우에는 필요하지 않아요:
- 다른 동적 테이블에서 직접 읽는 경우(같은 계정: 둘 다 자동으로 같은 파이프라인에 조인)
- 공유된 동적 테이블에서 직접 읽는 경우(공유 경계가 이미 파이프라인을 분리)
- 기본 테이블만 의존성으로 포함하는 뷰에서 읽는 경우
| 시나리오 | 경계 필요? | 이유 |
|---|---|---|
| 동적 테이블이 다른 동적 테이블에서 직접 읽음 | 아니요 (분리용으로 선택적) | 같은 파이프라인, 조정된 스케줄링 |
| 의존성에 동적 테이블이 없는 뷰에서 동적 테이블이 읽음 | 아니요 (방어적, 선택적) | 뷰는 기본 테이블 위의 SQL 오버레이 |
| 의존성에 동적 테이블이 있는 뷰에서 동적 테이블이 읽음 | 예 | 뷰의 동적 테이블 의존성을 가로질러 조정할 수 없음 |
| 동적 테이블이 공유된 동적 테이블에서 읽음 | 아니요 | 공유 경계가 이미 분리 |
| 동적 테이블 의존성이 없는 공유 뷰에서 동적 테이블이 읽음 | 아니요 (방어적, 선택적) | 공유 기본 테이블과 동일 |
| 동적 테이블 의존성이 있는 공유 뷰에서 동적 테이블이 읽음 | 예 | 경계를 가로질러 같은 조정 문제 |
방어적 경계 감싸기
구현을 통제하지 않는 뷰에서 읽을 때 DYNAMIC_TABLE_REFRESH_BOUNDARY()로 감싸는 것을 고려하세요. 경계가 있으면 동적 테이블의 갱신 그래프가 감싼 참조에서 멈춰요. 갱신 시점의 현재 상태를 읽으며 그 뒤에 무엇이 있는지는 추적하지 않아요. 기본 테이블의 변경 추적 비활성화나 재구성된 중간 의존성 같은 경계 아래의 변경은 파이프라인의 갱신을 막지 않아요.
경계는 참조된 객체 자체에 대한 접근 필요성을 제거하지 않아요. 객체가 삭제되거나 그에 대한 권한이 해지되면 동적 테이블은 여전히 실패해요.
절충점: 입력을 감싸면 더 이상 파이프라인의 나머지와 함께 갱신되지 않아요. 감싼 뷰를 다른 입력과 조인하면 두 측이 서로 다른 시점을 반영할 수 있어요.
경계 입력이 지연되면 어떤 일이 일어나는가
갱신 경계가 두 파이프라인을 분리하면 업스트림 측의 실패는 전파되지 않아요. 다운스트림 테이블은 여전히 자신의 스케줄로 갱신되고 경계 입력의 마지막 사용 가능한 버전을 읽어요. 즉:
- 다운스트림 테이블은 갱신 상태의 경고 없이 지연된 경계 입력 데이터에 기반한 결과를 만들 수 있어요.
- 갱신 경계를 사용할 때는 다운스트림 파이프라인만 모니터링하는 것으로는 충분하지 않아요. 경계 측의 지연을 감지하려면 업스트림 파이프라인을 독립적으로 모니터링해야 해요.
- 다운스트림 테이블의 자체 갱신은 정상적으로 완료됐으므로
SUCCEEDED를 보여줘요.
갱신 경계는 교차 파이프라인 일관성을 희생하고 캐스케이딩 지연을 방지해요. 스냅샷 격리가 필요 없는 의존성에서만 사용하세요.
갱신 경계가 있는 뷰 사용 (지연 뷰 패턴)
이 패턴은 같은 조직의 다른 팀이 데이터를 공유할 때 흔해요. 갱신 경계를 통해 한 팀의 파이프라인이 독립적으로 운영되어, 한 팀 파이프라인의 실패나 지연이 다른 팀의 다운스트림 테이블로 캐스케이드되지 않게 해요.
동적 테이블은 다른 동적 테이블을 쿼리하는 뷰를 DYNAMIC_TABLE_REFRESH_BOUNDARY()로 감싸지 않는 한 읽을 수 없어요. 이 패턴은 다운스트림 테이블이 갱신 시점에 뷰가 해결하는 어떤 버전이든 보기 때문에 때로 "지연 뷰(delayed view)"라고 불려요. 조정된 스냅샷이 아니에요.
-- 동적 테이블 위의 뷰
CREATE OR REPLACE VIEW v_completed_orders AS
SELECT * FROM dt_orders WHERE order_status = 'completed';
-- 경계를 가진 뷰를 통해 읽는 동적 테이블
CREATE OR REPLACE DYNAMIC TABLE dt_completed_order_summary
TARGET_LAG = '15 minutes'
WAREHOUSE = transform_wh
AS
SELECT
customer_id,
COUNT(*) AS completed_count,
SUM(line_total) AS completed_revenue
FROM
DYNAMIC_TABLE_REFRESH_BOUNDARY(v_completed_orders)
GROUP BY customer_id;
경계 감싸기가 없으면 Snowflake가 뷰의 동적 테이블 의존성을 단일 조정된 파이프라인으로 해결할 수 없으므로 CREATE DYNAMIC TABLE 문이 오류를 반환해요. 이는 공유된 보안 뷰에도 적용돼요. 공급자가 동적 테이블을 의존성으로 포함하는 보안 뷰를 공유하면, 소비자는 이를 DYNAMIC_TABLE_REFRESH_BOUNDARY()로 감싸야 해요.
공유 관련 파이프라인 경계 지침은 동적 테이블을 참조하는 공유 뷰 문서를 참조하세요.
파이프라인 경계 확인
DYNAMIC_TABLE_GRAPH_HISTORY를 사용해 어떤 테이블이 파이프라인을 공유하는지, 어떤 것이 경계로 분리되는지 확인하세요. 같은 파이프라인의 테이블은 같은 그래프를 공유하고 조정된 스케줄링 상태를 가져요.
SELECT
name,
scheduling_state:"state"::STRING AS scheduling_state,
target_lag_type,
target_lag_sec,
ARRAY_TO_STRING(
TRANSFORM(inputs, o -> o:"name"::STRING),
', '
) AS inputs
FROM
TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_GRAPH_HISTORY())
WHERE
name IN ('MYDB.MYSCHEMA.DT_ORDERS', 'MYDB.MYSCHEMA.DT_ORDERS_DAILY', 'MYDB.MYSCHEMA.DT_ENRICHED_ORDERS')
ORDER BY name;
+------------------------------------+------------------+-----------------+----------------+-----------------------------------------------------+
| NAME | SCHEDULING_STATE | TARGET_LAG_TYPE | TARGET_LAG_SEC | INPUTS |
|------------------------------------+------------------+-----------------+----------------+-----------------------------------------------------|
| MYDB.MYSCHEMA.DT_ENRICHED_ORDERS | ACTIVE | USER_DEFINED | 300 | MYDB.MYSCHEMA.DT_ORDERS |
| MYDB.MYSCHEMA.DT_ORDERS_DAILY | ACTIVE | USER_DEFINED | 1800 | MYDB.MYSCHEMA.DT_ORDERS, MYDB.MYSCHEMA.DIM_CUSTOMERS |
| MYDB.MYSCHEMA.DT_ORDERS | ACTIVE | USER_DEFINED | 600 | MYDB.MYSCHEMA.RAW_ORDERS |
+------------------------------------+------------------+-----------------+----------------+-----------------------------------------------------+
💡 위 쿼리는 경계 입력을 구분하지 않아요. 원시 JSON에서 경계 입력은
insideRefreshBoundary:true로 나타나요. 이 파이프라인에서 기대한 테이블이 출력에 없으면 그 정의에DYNAMIC_TABLE_REFRESH_BOUNDARY()감싸기가 있는지 확인하세요.
갱신 경계 제한 사항
단일 정의에서 같은 업스트림 동적 테이블에 대한 모든 참조는 일관되게 직접이거나 일관되게 DYNAMIC_TABLE_REFRESH_BOUNDARY()로 감싸야 해요. 둘을 섞으면 CREATE DYNAMIC TABLE이 실패해요.
경계 target은 명명된 객체여야 함. DYNAMIC_TABLE_REFRESH_BOUNDARY()는 테이블, 뷰, 동적 테이블, 또는 CTE(common table expression)를 감싸요. 인라인 서브쿼리, 테이블 함수, UDTF(사용자 정의 테이블 함수)는 감쌀 수 없어요.
동적 테이블 밖에서는 효과가 없음. 일반 SELECT 쿼리에서 DYNAMIC_TABLE_REFRESH_BOUNDARY()를 호출할 수 있지만, 동적 테이블 정의 밖에서는 효과가 없어요.
다음 단계
- Snowflake가
INCREMENTAL또는FULL갱신을 선택하는 방법을 이해하려면, 동적 테이블 갱신 모드 문서를 참조하세요. - 지연이 target lag을 초과할 때 알림을 설정하려면, 동적 테이블 모니터링 문서를 참조하세요.
UPSTREAM_FAILED및 기타 갱신 실패를 진단하려면, 동적 테이블 갱신 문제 트러블슈팅 문서를 참조하세요.