스냅샷(Snapshots)으로 데이터 변화 기록하기
스냅샷(Snapshots)으로 데이터 변화 기록하기
분석을 하다 보면 테이블이 "지금"의 상태만 담고 있어서 과거의 상태를 알 수 없을 때가 있어요. 예를 들어 주문 상태가 pending에서 shipped로 바뀌면, 그전에 pending이었던 사실은 사라져 버리죠. dbt의 **스냅샷(snapshot)**은 이런 가변(mutable) 테이블의 변화를 시간에 따라 기록해 주는 메커니즘이에요. 스냅샷은 가변 소스 테이블에 **타입-2 서서히 변하는 차원(SCD, Slowly Changing Dimension)**을 구현해서, 행의 값이 시간에 따라 어떻게 바뀌는지 추적해 줘요.
스냅샷이 필요한 상황
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_from과 dbt_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_schema나target_database를 정의하지 않으면 스냅샷은generate_schema_name또는generate_database_name매크로를 사용해 어디에 빌드할지 결정해요. - 개발자는 다른 리소스 타입과 동일하게
schema와database설정으로 커스텀 위치를 정할 수도 있어요. tags나post-hook같은 다른 설정도 여럿 지원돼요. 전체 목록은 Snapshot configurations 문서를 참고해요.- 스냅샷은
dbt_project.yml과config블록 어느 쪽에서도 설정할 수 있어요. - dbt v1.12부터는
dbt compile명령으로 스냅샷에 대해 생성된 SQL을 확인할 수 있어요. 컴파일된 SQL 파일은 프로젝트의target/compiled/디렉터리에서 찾을 수 있어요.
스냅샷 프로젝트에 추가하기
dbt v1.9 이상 기준으로 스냅샷을 프로젝트에 추가하는 단계를 볼게요. 1.8 이하 버전을 쓴다면 Legacy snapshot configurations 문서를 참고해야 해요.
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`.
- (선택) 임시(ephemeral) 모델로 변환을 적용해요. 기본적으로 스냅샷은 소스를 직접 참조해요. 필터링이나 중복 제거 같은 변환이 필요하면, 그 변환을 적용한 임시 모델을 먼저 정의하고 스냅샷의
relation필드에서source()대신 그 모델을 참조해요. 예를 들어 이런 임시 모델이 있다면:
{{ config(materialized='ephemeral') }}
select * from {{ source('jaffle_shop', 'orders') }}
relation 필드에서 임시 모델을 이렇게 참조해요.
snapshots:
- name: orders_snapshot
relation: ref('ephemeral_orders')
... rest of config...
-
쿼리 결과 집합에 레코드가 마지막으로 갱신된 시점을 나타내는 신뢰할 만한 타임스탬프 컬럼이 있는지 확인해요. 이 예시에서는
updated_at컬럼이 레코드 변화를 신뢰성 있게 나타내므로timestamp전략을 쓸 수 있어요. 신뢰할 만한 타임스탬프가 없다면check전략을 써야 하는데, 자세한 내용은 아래에서 다룰게요. -
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
-
dbt가 만든 테이블(
analytics.snapshots.orders_snapshot)을 select해서 결과를 확인해요. 첫 실행 후에는 쿼리 결과에 더해 아래에서 설명할 스냅샷 메타 필드가 보일 거예요. -
dbt snapshot명령을 다시 실행하고 결과를 확인해요. 갱신된 레코드가 있다면 스냅샷이 그 변화를 반영할 거예요. -
하위 모델에서
ref함수로 스냅샷을 참조해요.
select * from {{ ref('orders_snapshot') }}
- 스냅샷은 자주 실행해야만 의미가 있어요 —
dbt snapshot명령이 주기적으로 실행되도록 스케줄을 잡아 두세요.
설정 모범 사례
- 가능하면 timestamp 전략을 써요. timestamp 전략은
check전략보다 컬럼 추가·삭제를 더 효율적으로 처리해요. 단일updated_at필드에 의존하므로 소스 테이블이 진화해도 스냅샷 구성을 계속 갱신할 필요가 없어요. 컬럼 하나만 추적하면 되고, 소스 테이블의 새/제거된 컬럼을 자동으로 처리하며, 스키마가 진화할 때 오류 가능성이 더 낮아요. dbt_valid_to_current로 날짜 구간 쿼리를 쉽게 만들어요. 기본적으로 현재 레코드의dbt_valid_to는NULL이에요. 하지만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에 내장된 전략은 두 가지예요.
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_from과 dbt_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