스냅샷(Snapshots)으로 데이터 변화 기록하기

스냅샷(Snapshots)으로 데이터 변화 기록하기

분석을 하다 보면 테이블이 "지금"의 상태만 담고 있어서 과거의 상태를 알 수 없을 때가 있어요. 예를 들어 주문 상태가 pending에서 shipped로 바뀌면, 그전에 pending이었던 사실은 사라져 버리죠. dbt의 **스냅샷(snapshot)**은 이런 가변(mutable) 테이블의 변화를 시간에 따라 기록해 주는 메커니즘이에요. 스냅샷은 가변 소스 테이블에 **타입-2 서서히 변하는 차원(SCD, Slowly Changing Dimension)**을 구현해서, 행의 값이 시간에 따라 어떻게 바뀌는지 추적해 줘요.

출처: dbt 공식 문서 — Add snapshots to your DAG

스냅샷이 필요한 상황

status 필드가 주문 처리 과정에서 덮어써질 수 있는 orders 테이블이 있다고 상상해 볼게요.

id status updated_at
1 pending 2024-01-01

이제 주문이 "pending"에서 "shipped"로 바뀌었다고 해 볼게요. 같은 레코드는 이렇게 보일 거예요.

id status updated_at
1 shipped 2024-01-02

주문은 이제 "shipped" 상태지만, 언제 "pending"이었는지에 대한 정보는 잃어버렸어요. 이렇게 되면 주문이 배송되기까지 얼마나 걸렸는지 분석하기 어려워지거나 불가능해져요. dbt는 이런 변화를 "스냅샷" 해서 행의 값이 시간에 따라 어떻게 변하는지 이해하게 해 줘요. 위 예시의 스냅샷 테이블은 이렇게 생겨요.

id status updated_at dbt_valid_from dbt_valid_to
1 pending 2024-01-01 2024-01-01 2024-01-02
1 shipped 2024-01-02 2024-01-02 null

이제 dbt_valid_fromdbt_valid_to로 "이 상태는 이 기간 동안 유효했다"는 걸 정확히 표현할 수 있어요.

스냅샷 설정하기

dbt v1.9부터는 스냅샷을 YAML 파일에서 설정해요. 모델과 나란히, 더 깔끔하고 빠르고 일관된 방식으로 스냅샷 구성을 정의할 수 있어요. 스냅샷 YAML 파일은 models 디렉터리나 snapshots 디렉터리에 두면 돼요.

snapshots:
  - name: string
    relation: relation # source('my_source', 'my_table') or ref('my_model')
    description:  markdown_string
    config:
      database: string
      schema: string
      alias: string
      strategy: timestamp | check
      unique_key: column_name_or_expression
      check_cols: [column_name] | all
      updated_at: column_name
      snapshot_meta_column_names: dictionary
      dbt_valid_to_current: string
      hard_deletes: ignore | invalidate | new_record 

스냅샷에 사용할 수 있는 설정을 표로 정리했어요.

Config Description Required? Example
database Specify a custom database for the snapshot No analytics
schema Specify a custom schema for the snapshot No snapshots
alias Specify an alias for the snapshot No your_custom_snapshot
strategy The snapshot strategy to use. Valid values: timestamp or check Yes timestamp
unique_key A primary-key column(s) (string or array) or expression for the record Yes id or [order_id, product_id]
check_cols If using the check strategy, then the columns to check Only if using the check strategy ["status"]
updated_at A column in your snapshot query results that indicates when each record was last updated, used in the timestamp strategy. May support ISO date strings and unix epoch integers, depending on the data platform you use. Only if using the timestamp strategy updated_at
dbt_valid_to_current Set a custom indicator for the value of dbt_valid_to in current snapshot records (like a future date). By default, this value is NULL. When configured, dbt will use the specified value instead of NULL for dbt_valid_to for current records in the snapshot table. No string
snapshot_meta_column_names Customize the names of the snapshot meta fields No dictionary
hard_deletes Specify how to handle deleted rows from the source. Supported options are ignore (default), invalidate (replaces the legacy invalidate_hard_deletes=true), and new_record. No string

몇 가지 참고할 점이 있어요.

  • v1.9부터 target_schema는 선택 사항이 되었고, 스냅샷이 환경을 인식하게 됐어요. target_schematarget_database를 정의하지 않으면 스냅샷은 generate_schema_name 또는 generate_database_name 매크로를 사용해 어디에 빌드할지 결정해요.
  • 개발자는 다른 리소스 타입과 동일하게 schemadatabase 설정으로 커스텀 위치를 정할 수도 있어요.
  • tagspost-hook 같은 다른 설정도 여럿 지원돼요. 전체 목록은 Snapshot configurations 문서를 참고해요.
  • 스냅샷은 dbt_project.ymlconfig 블록 어느 쪽에서도 설정할 수 있어요.
  • dbt v1.12부터는 dbt compile 명령으로 스냅샷에 대해 생성된 SQL을 확인할 수 있어요. 컴파일된 SQL 파일은 프로젝트의 target/compiled/ 디렉터리에서 찾을 수 있어요.

스냅샷 프로젝트에 추가하기

dbt v1.9 이상 기준으로 스냅샷을 프로젝트에 추가하는 단계를 볼게요. 1.8 이하 버전을 쓴다면 Legacy snapshot configurations 문서를 참고해야 해요.

  1. snapshots 디렉터리에 속성 YAML 파일을 만들어요 — snapshots/orders_snapshot.yml에 설정을 적어요. 프로젝트 YAML 파일(dbt_project.yml)에서도 설정할 수 있어요.
snapshots:
  - name: orders_snapshot
    relation: source('jaffle_shop', 'orders')
    config:
      schema: snapshots
      database: analytics
      unique_key: id
      strategy: timestamp
      updated_at: updated_at
      dbt_valid_to_current: "to_date('9999-12-31')" # Specifies that current records should have `dbt_valid_to` set to `'9999-12-31'` instead of `NULL`.
  1. (선택) 임시(ephemeral) 모델로 변환을 적용해요. 기본적으로 스냅샷은 소스를 직접 참조해요. 필터링이나 중복 제거 같은 변환이 필요하면, 그 변환을 적용한 임시 모델을 먼저 정의하고 스냅샷의 relation 필드에서 source() 대신 그 모델을 참조해요. 예를 들어 이런 임시 모델이 있다면:
{{ config(materialized='ephemeral') }}

select * from {{ source('jaffle_shop', 'orders') }}

relation 필드에서 임시 모델을 이렇게 참조해요.

  snapshots:
    - name: orders_snapshot
      relation: ref('ephemeral_orders')
      ... rest of config...
  1. 쿼리 결과 집합에 레코드가 마지막으로 갱신된 시점을 나타내는 신뢰할 만한 타임스탬프 컬럼이 있는지 확인해요. 이 예시에서는 updated_at 컬럼이 레코드 변화를 신뢰성 있게 나타내므로 timestamp 전략을 쓸 수 있어요. 신뢰할 만한 타임스탬프가 없다면 check 전략을 써야 하는데, 자세한 내용은 아래에서 다룰게요.

  2. dbt snapshot 명령을 실행하면, 예시 기준으로 analytics.snapshots.orders_snapshot에 새 테이블이 만들어져요. schema 설정은 generate_schema_name 매크로를 활용해요.

$ dbt snapshot
Running with dbt=1.9.0

15:07:36 | Concurrency: 8 threads (target='dev')
15:07:36 |
15:07:36 | 1 of 1 START snapshot snapshots.orders_snapshot...... [RUN]
15:07:36 | 1 of 1 OK snapshot snapshots.orders_snapshot..........[SELECT 3 in 1.82s]
15:07:36 |
15:07:36 | Finished running 1 snapshots in 0.68s.

Completed successfully

Done. PASS=2 ERROR=0 SKIP=0 TOTAL=1
  1. dbt가 만든 테이블(analytics.snapshots.orders_snapshot)을 select해서 결과를 확인해요. 첫 실행 후에는 쿼리 결과에 더해 아래에서 설명할 스냅샷 메타 필드가 보일 거예요.

  2. dbt snapshot 명령을 다시 실행하고 결과를 확인해요. 갱신된 레코드가 있다면 스냅샷이 그 변화를 반영할 거예요.

  3. 하위 모델에서 ref 함수로 스냅샷을 참조해요.

select * from {{ ref('orders_snapshot') }}
  1. 스냅샷은 자주 실행해야만 의미가 있어요 — dbt snapshot 명령이 주기적으로 실행되도록 스케줄을 잡아 두세요.

설정 모범 사례

  • 가능하면 timestamp 전략을 써요. timestamp 전략은 check 전략보다 컬럼 추가·삭제를 더 효율적으로 처리해요. 단일 updated_at 필드에 의존하므로 소스 테이블이 진화해도 스냅샷 구성을 계속 갱신할 필요가 없어요. 컬럼 하나만 추적하면 되고, 소스 테이블의 새/제거된 컬럼을 자동으로 처리하며, 스키마가 진화할 때 오류 가능성이 더 낮아요.
  • dbt_valid_to_current로 날짜 구간 쿼리를 쉽게 만들어요. 기본적으로 현재 레코드의 dbt_valid_toNULL이에요. 하지만 dbt_valid_to_current(dbt v1.9+에서 사용 가능)를 설정하면 현재 레코드의 dbt_valid_to가 지정한 값(예: 9999-12-31)이 되어 날짜 구간 필터링이 간단해져요.
  • unique key가 정말로 고유한지 확인해요. unique key는 dbt가 행을 매칭하는 데 쓰이므로 실제로 고유해야 해요. 소스를 스냅샷한다면 소스에 유니크 테스트를 추가하는 걸 권장해요.
  • 모델 스키마와 분리된 스키마를 써요. 스냅샷은 다시 빌드할 수 없어요. 그래서 스냅샷을 별도 스키마에 두어 사용자가 이게 특별하다는 걸 알게 하는 게 좋아요. 모델과 다른 권한을 주고, 다른 사용자(또는 웨어하우스에 따라 다른 역할)로 실행해서 실수로 스냅샷을 지우기 어렵게 만들 수 있어요.
  • 스냅샷 전에 정리·변환하려면 임시 모델을 써요. 스냅샷 전에 데이터를 정리하거나 변환해야 한다면, 필요한 변환을 적용한 임시/스테이징 모델을 만든 뒤 스냅샷 구성에서 그 모델을 참조해요. 이렇게 하면 스냅샷 정의가 깔끔해지고 변환을 따로 테스트·실행할 수 있어요.

스냅샷이 동작하는 방식

dbt snapshot 명령을 실행하면:

  • 첫 실행: dbt가 초기 스냅샷 테이블을 만들어요. select 문의 결과 집합에 dbt_valid_from, dbt_valid_to 같은 컬럼이 추가된 형태예요. 모든 레코드는 dbt_valid_to = null 값을 가지거나, dbt_valid_to_current(dbt v1.9+)가 설정돼 있으면 그 지정 값이 돼요.
  • 이후 실행: dbt가 어떤 레코드가 바뀌었는지, 새 레코드가 생겼는지 확인해요.
    • 바뀐 기존 레코드에 대해서는 dbt_valid_to 컬럼을 갱신해요.
    • 바뀐 레코드와 새 레코드는 스냅샷 테이블에 삽입돼요. 이 레코드들은 이제 dbt_valid_to = null이거나 dbt_valid_to_current에 설정된 값이 돼요.

참고할 점:

  • 이 컬럼 이름들은 snapshot_meta_column_names 설정으로 팀이나 조직 규약에 맞게 커스터마이즈할 수 있어요.
  • dbt_valid_to_current 설정으로 현재 스냅샷 레코드의 dbt_valid_to 값을 커스텀 지시자(미래 날짜처럼)로 정할 수 있어요. 기본값은 NULL이에요.
  • hard_deletes 설정으로 소스에서 행이 "삭제"될 때 새 레코드를 추가해 하드 삭제를 추적할 수 있어요. 지원 옵션은 ignore, invalidate, new_record예요.

스냅샷은 일반 모델을 참조하는 것과 같은 방식 — ref 함수 — 으로 하위 모델에서 참조할 수 있어요.

행 변화 감지: 전략(Strategies)

스냅샷 "전략(strategy)"은 dbt가 행이 바뀌었는지 어떻게 아는지 정의해요. dbt에 내장된 전략은 두 가지예요.

  • Timestampupdated_at 컬럼으로 행 변화 여부를 판단해요.
  • Check — 컬럼 목록을 현재 값과 과거 값 사이에서 비교해 행 변화 여부를 판단해요.

Timestamp 전략 (권장)

timestamp 전략은 updated_at 필드로 행이 바뀌었는지 판단해요. 설정한 updated_at 컬럼이 마지막 스냅샷 실행 시점보다 최신이면, dbt는 옛 레코드를 무효화하고 새 레코드를 기록해요. 타임스탬프가 안 바뀌었다면 아무 동작도 하지 않아요.

timestamp 전략이 권장되는 이유:

  • 컬럼 하나(updated_at)만 추적하면 돼요.
  • 소스 테이블의 새/제거된 컬럼을 자동으로 처리해요.
  • 테이블 스키마가 진화할 때 오류 가능성이 낮아요 (예를 들어 check 전략을 쓰면 check_cols 구성을 계속 갱신해야 할 수 있어요).

timestamp 전략이 필요한 설정:

Config Description Example
updated_at A column which represents when the source row was last updated. May support ISO date strings and unix epoch integers, depending on the data platform you use. updated_at

사용 예시:

snapshots:
  - name: orders_snapshot_timestamp
    relation: source('jaffle_shop', 'orders')
    config:
      schema: snapshots
      unique_key: id
      strategy: timestamp
      updated_at: updated_at

Check 전략

check 전략은 신뢰할 만한 updated_at 컬럼이 없는 테이블에 유용해요. 이 전략은 컬럼 목록을 현재 값과 과거 값 사이에서 비교해 동작해요. 이 컬럼 중 하나라도 바뀌면 dbt가 옛 레코드를 무효화하고 새 레코드를 기록해요. 컬럼 값이 동일하다면 아무 동작도 하지 않아요.

check 전략이 필요한 설정:

Config Description Example
check_cols A list of columns to check for changes, or all to check all columns ["name", "email"]

:::caution check_cols = 'all' check 스냅샷 전략은 check_cols = 'all'을 주면 모든 컬럼의 변화를 추적하도록 설정할 수 있어요. 하지만 확인하려는 컬럼을 명시적으로 열거하는 편이 더 좋아요. 많은 컬럼을 하나로 합치려면 서로게이트 키(surrogate key)를 고려해 보세요. :::

사용 예시:

snapshots:
  - name: orders_snapshot_check
    relation: source('jaffle_shop', 'orders')
    config:
      schema: snapshots
      unique_key: id
      strategy: check
      check_cols:
        - status
        - is_cancelled

updated_at과 함께 쓰는 예시

check 전략을 쓸 때 dbt는 check_cols의 값을 비교해 변화를 추적해요. 기본적으로 dbt는 타임스탬프로 dbt_updated_at, dbt_valid_from, dbt_valid_to 필드를 갱신해요. 선택적으로 updated_at 컬럼을 설정할 수 있어요.

  • updated_at이 설정돼 있으면 timestamp 전략처럼 이 컬럼을 사용해요.
  • updated_at 값이 null이면 dbt는 기본적으로 현재 타임스탬프를 사용해요.

updated_at과 함께 check 전략을 쓰는 예시를 볼게요.

snapshots:
  - name: orders_snapshot
    relation: ref('stg_orders')
    config:
      schema: snapshots
      unique_key: order_id
      strategy: check
      check_cols:
        - status
        - is_cancelled
      updated_at: updated_at

이 예시에서:

  • 지정한 check_cols 중 하나라도 바뀌면 스냅샷이 새 행을 만들어요. updated_at 컬럼에 값이 있으면(not null) 그 값을 쓰고, 없으면 타임스탬프를 기본값으로 써요.
  • updated_at을 설정하지 않으면 dbt가 자동으로 현재 타임스탬프로 폴백해 변화를 추적해요.
  • updated_at 컬럼이 레코드 갱신을 추적하기에 신뢰할 만하지 않지만, 행 변화가 감지될 때 스냅샷 실행 시각보다는 그 컬럼 값을 쓰고 싶다면 이 방식이 유용해요.

하드 삭제(Hard deletes, opt-in)

dbt v1.9 이상에서는 hard_deletes 설정이 invalidate_hard_deletes 설정을 대체해, 소스에서 삭제된 행을 어떻게 처리할지 더 세밀하게 제어할 수 있어요. hard_deletes는 별도 전략이 아니라 어떤 스냅샷 전략과도 함께 쓸 수 있는 추가 옵션 기능이에요.

Field Description
ignore (default) No action for deleted records.
invalidate Behaves the same as the existing invalidate_hard_deletes=true, where deleted records are invalidated by setting dbt_valid_to.
new_record Tracks deleted records as new rows using the dbt_is_deleted meta field when records are deleted.

사용 예시:

snapshots:
  - name: orders_snapshot_hard_delete
    relation: source('jaffle_shop', 'orders')
    config:
      schema: snapshots
      unique_key: id
      strategy: timestamp
      updated_at: updated_at
      hard_deletes: new_record  # options are: 'ignore', 'invalidate', or 'new_record'

이 예시에서 hard_deletes: new_record 설정은 삭제된 레코드에 대해 dbt_is_deleted 컬럼을 True로 설정한 새 행을 추가해요. 복원된 레코드는 dbt_is_deleted 필드가 False인 새 행으로 추가돼요.

결과 테이블은 이렇게 생겨요.

id status updated_at dbt_valid_from dbt_valid_to dbt_is_deleted
1 pending 2024-01-01 10:47 2024-01-01 10:47 2024-01-01 11:05 False
1 shipped 2024-01-01 11:05 2024-01-01 11:05 2024-01-01 11:20 False
1 deleted 2024-01-01 11:20 2024-01-01 11:20 2024-01-01 12:00 True
1 restored 2024-01-01 12:00 2024-01-01 12:00 False

스냅샷 메타 필드

스냅샷 테이블은 소스 데이터셋의 복제본에 몇 가지 메타 필드가 더해진 형태로 만들어져요.

dbt v1.9 이상에서:

  • 이 컬럼 이름들은 snapshot_meta_column_names 설정으로 팀/조직 규약에 맞게 커스터마이즈할 수 있어요.
  • dbt_valid_to_current 설정으로 현재 스냅샷 레코드의 dbt_valid_to 값을 커스텀 지시자(미래 날짜처럼 9999-12-31)로 정할 수 있어요. 기본값은 NULL이에요.
  • hard_deletes 설정으로 hard_deletes='new_record'를 쓸 때 삭제된 레코드를 dbt_is_deleted 메타 필드가 있는 새 행으로 추적할 수 있어요.
Field Meaning Notes Example
dbt_valid_from The timestamp when this snapshot row became valid. Use this to order versions of a record. For a given row, this often matches dbt_updated_at, but they mean different things. snapshot_meta_column_names: {dbt_valid_from: start_date}
dbt_valid_to The timestamp when this row became invalidated. For current records, this is NULL by default or the value specified in dbt_valid_to_current. The most recent snapshot record will have dbt_valid_to set to NULL or the specified value. snapshot_meta_column_names: {dbt_valid_to: end_date}
dbt_scd_id A unique key generated for each snapshot row. Used internally by dbt to identify each SCD version. snapshot_meta_column_names: {dbt_scd_id: scd_id}
dbt_updated_at The source record's change timestamp when this snapshot row was inserted. Used internally by dbt. snapshot_meta_column_names: {dbt_updated_at: modified_date}
dbt_is_deleted A string value indicating if the record has been deleted. (True if deleted, False if not deleted). Added when hard_deletes='new_record' is configured. snapshot_meta_column_names: {dbt_is_deleted: is_deleted}

이 컬럼 이름들은 모두 snapshot_meta_column_names 설정으로 커스터마이즈할 수 있어요. 삽입 시점에 dbt_valid_fromdbt_updated_at은 같은 값으로 설정돼요. 둘은 각각 유효성 시작 시점과 기록된 변경 시각을 나타내요.

전략별 메타 필드 타임스탬프

각 컬럼에 쓰이는 타임스탬프는 사용하는 전략에 따라 달라져요.

  • timestamp 전략에서는 설정한 updated_at 컬럼으로 dbt_valid_from, dbt_valid_to, dbt_updated_at 컬럼을 채워요.

    스냅샷 쿼리 결과가 2024-01-01 11:00에 이렇다고 해 볼게요.

    id status updated_at
    1 pending 2024-01-01 10:47

    스냅샷 결과(11:00은 어디에도 쓰이지 않음에 주목):

    id status updated_at dbt_valid_from dbt_valid_to dbt_updated_at
    1 pending 2024-01-01 10:47 2024-01-01 10:47 2024-01-01 10:47

    2024-01-01 11:30의 쿼리 결과:

    id status updated_at
    1 shipped 2024-01-01 11:05

    스냅샷 결과(11:30은 어디에도 쓰이지 않음):

    id status updated_at dbt_valid_from dbt_valid_to dbt_updated_at
    1 pending 2024-01-01 10:47 2024-01-01 10:47 2024-01-01 11:05 2024-01-01 10:47
    1 shipped 2024-01-01 11:05 2024-01-01 11:05 2024-01-01 11:05

    hard_deletes='new_record'가 적용된 스냅샷 결과:

    id status updated_at dbt_valid_from dbt_valid_to dbt_updated_at dbt_is_deleted
    1 pending 2024-01-01 10:47 2024-01-01 10:47 2024-01-01 11:05 2024-01-01 10:47 False
    1 shipped 2024-01-01 11:05 2024-01-01 11:05 2024-01-01 11:20 2024-01-01 11:05 False
    1 deleted 2024-01-01 11:20 2024-01-01 11:20 2024-01-01 11:20 True
  • check 전략에서는 현재 타임스탬프로 각 컬럼을 채워요. 설정돼 있으면 timestamp 전략처럼 updated_at 컬럼을 대신 사용해요.

    스냅샷 쿼리 결과가 2024-01-01 11:00에 이렇다고 해 볼게요.

    id status
    1 pending

    스냅샷 결과:

    id status dbt_valid_from dbt_valid_to dbt_updated_at
    1 pending 2024-01-01 11:00 2024-01-01 11:00

    2024-01-01 11:30의 쿼리 결과:

    id status
    1 shipped

    스냅샷 결과:

    id status dbt_valid_from dbt_valid_to dbt_updated_at
    1 pending 2024-01-01 11:00 2024-01-01 11:30 2024-01-01 11:00
    1 shipped 2024-01-01 11:30 2024-01-01 11:30

    hard_deletes='new_record'가 적용된 스냅샷 결과:

    id status dbt_valid_from dbt_valid_to dbt_updated_at dbt_is_deleted
    1 pending 2024-01-01 11:00 2024-01-01 11:30 2024-01-01 11:00 False
    1 shipped 2024-01-01 11:30 2024-01-01 11:40 2024-01-01 11:30 False
    1 deleted 2024-01-01 11:40 2024-01-01 11:40 True

더 알아보기