dbt에서의 Surrogate Key 생성 전략
Surrogate Key 개념부터, dbt에서 Surrogate Key 생성 전략에 대해 정리하였습니다.
Jun 07, 2025

dbt에서의 Surrogate Key 생성 전략
dbt에서의 Surrogate Key 생성 전략🧩 Surrogate Key란?Hash 기반 Surrogate Key (dbt 권장 방식)MIISK 방식 (정수형 순차 surrogate key)💡 왜 dbt에서 Incremental Integer Surrogate Key가 어려운가?키 조합 vs Surrogate Key에 대해서Snowflake SEQUENCE 기반 Surrogate Key 자동화 매크로 (MIISK 방식)해시 기반 Surrogate Key (dbt_utils 활용)SCD Type 2 적용 예시 (surrogate key + hash 활용)최종 요약참고
🧩 Surrogate Key란?
- 데이터 웨어하우스에서 조인 성능을 높이고, 변화 추적(SCD 등)을 위해 사용되는 의미 없는 대체 키입니다.
자연 키(Natural Key)
와 달리, 비즈니스 의미 없이 단순히 데이터 식별자 역할만 수행합니다.
- 고전적인 사용 방법으로는 주로 정수형이었습니다.
- 예: user_sk = 123456
(참고용 1) 🌱 Natural Key란?
- 운영 시스템에서 이미 존재하는 고유 식별자로, 비즈니스적으로 의미가 있는 키입니다.
- 예: 주민등록번호, 이메일, 고객 ID, 제품 코드 등
- 장점: 기존 시스템과의 연동이 쉬움
- 단점: 변경될 가능성, 중복 위험, 복잡한 조합 등으로 인해 DW에선 보통 Surrogate Key로 대체합니다.
(참고용 2) 🌱 Durable Key란?
- 한 엔티티를 일관되게 식별하기 위한 불변의 식별자입니다.
- Surrogate Key와 비슷하지만, SCD(Slowly Changing Dimension) 처리 등으로 속성이 바뀌더라도 ID는 유지됩니다.
- 보통 ETL이 할당하고 관리하며, supernatural key, persistent identifier라고도 부릅니다.
- 많이 사용하는 예제 (uuid)
- UUID는 전역에서 유일(unique)하므로, 여러 시스템을 통합할 때 고유 ID 보장에 매우 유리합니다.
- 특히, 고객 ID나 글로벌 식별자처럼 시간이 지나도 변하지 않아야 하는 key를 만들기에 적합합니다.
- 다만 16바이트라는 용량으로 인해, 스토리지 부담이 존재합니다.
🔷 Surrogate Key 생성 전략 비교 요약
전략 | 설명 | 장점 | 단점 | 사용 적합 |
MIISK
(정수형 순차 surrogate key) | 1, 2, 3, ... 처럼 생성되는 정수 기반 surrogate key
(예: Snowflake SEQUENCE 사용) | - 성능 최적화
- 외부 시스템 연동 용이
- 전통적 방식 | - 병렬 실행 시 충돌/갭 발생
- View나 재실행에는 부적합
- 관리 복잡 | 정수형 키가 필요한
외부 연동 또는 복잡한 SCD |
Hash 기반 Surrogate Key | 여러 컬럼을 조합해 md5 해시로 생성 (dbt_utils.generate_surrogate_key) | - 멱등성(idempotent)
- 재실행에도 동일한 결과
- 유지보수 용이
- 병렬 처리 안전 | - 문자열 기반으로 성능/공간 약간 손해
- 이론상 충돌 가능성 | BI 분석, 내부 데이터 모델링,
대부분의 dbt 프로젝트에서 추천 |
Hash 기반 Surrogate Key (dbt 권장 방식)
사용 예:
select
{{ dbt_utils.generate_surrogate_key(['user_id', 'event_date']) }} as user_event_sk,
...
from {{ ref('stg_events') }}
특징
- 같은 입력은 항상 같은 출력을 생성 → 멱등성 보장
- 플랫폼에 관계없이 사용 가능 (Snowflake, BigQuery, Redshift 등)
- SCD Type 2 등의 변경 추적에도 강력
- dbt의 철학인
재현 가능성(idempotency)
과 잘 맞음
내부 동작:
md5(concat_ws('||', col1, col2, ...))
MIISK 방식 (정수형 순차 surrogate key)
사용 예 (Snowflake + dbt)
📍meta config 등록 (schema.yml)
version: 2
models:
- name: dim_customers
description: 고객 차원 테이블
config:
meta:
surrogate_key: true
📍sequence 생성 (on-run-start macro):
create sequence if not exists {{ model.database }}.{{ model.schema }}.{{ model.name }}_seq;
📍사용:
select
{{ increment_sequence() }} as customer_id,
...
장점
- 외부 시스템(PK 등)과 연동 시 유리
- 정수형 조인 키로 성능 좋음
단점
- 실패 시 key gap 발생
- 병렬 실행 시 역순·중복 가능성
- 뷰(view)에는 사용할 수 없음
- 정렬 보장 필요 (ORDER BY 필수)
- DAG 구성이 더 복잡해짐
💡 왜 dbt에서 Incremental Integer Surrogate Key가 어려운가?
대략적으로 3가지 이유가 존재합니다.
- ROW_NUMBER() 사용
- 📌 문제점: 멱등성(idempotency)이 깨짐
- ROW_NUMBER()는 매번 실행할 때 정렬 기준이 바뀌면 같은 데이터라도 다른 surrogate key를 생성해요.
- 예를 들어, 어제는 A가 1번, B가 2번이었다면,
- → 이렇게 되면 downstream 모델에서 조인 불일치, SCD 실패, 이력 꼬임 등이 발생할 수 있어요.
ROW_NUMBER() OVER (ORDER BY something)
오늘은 B가 1번, A가 2번이 될 수도 있어요.
❗ dbt는 모든 모델 실행 결과가 재실행해도 동일해야(idempotent)
- SEQUENCE.NEXTVAL 사용 (ex: Snowflake)
SELECT my_schema.my_table_seq.NEXTVAL
🤔 문제점 1
- 실패 시 key gap 발생
- 모델이 실행되다가 중간에 실패하면:
- SEQUENCE는 값을 증가시킴 (예: 101 → 102)
- 하지만 테이블에 row는 insert되지 않음
- 다음 실행 시 → 다음 값(103)부터 시작 → 중간에 gap 생김
- 문제점
- SEQsaUENCE는 rollback 되지 않음
- key gap이 있으면 downstream 로직에서 의도치 않은 불일치가 발생할 수 있음
🤔 문제점 2
- 병렬 처리 환경에서 순서 보장 어려움
- Snowflake나 BigQuery는 쿼리를 병렬로 실행
- 각 쓰레드/워크로드가 NEXTVAL을 독립적으로 호출하면:
- key 순서가 뒤죽박죽이 될 수 있음
- 예: A row가 100, B row가 99 → 조인 순서 망가짐
- → 성능은 빠를 수 있지만, 순차성과 일관성을 요구하는 surrogate key에는 부적합
- IDENTITY 컬럼 사용
- dbt와 충돌
- dbt는 CREATE OR REPLACE 전략을 자주 씀 (materialized='table')
- 하지만 IDENTITY 컬럼은:
- 테이블이 재생성될 때마다 초기화됨
- 즉, 1부터 다시 시작 → 기존 key와 불일치 발생
- 즉, dbt의 기본 동작과 상충됨
- incremental table로 써도, append-only 구조가 아니면 관리가 복잡해짐
CREATE TABLE my_table (
id INT AUTOINCREMENT PRIMARY KEY,
...
)
📌 문제점
→ Snowflake는 IDENTITY 를 사용하는 테이블을 REPLACE 할 수 없음
3가지 이유를 정리하자면…
🔍 이유 | 설명 |
멱등성 요구 | 같은 입력 → 항상 같은 출력이 나와야 함 |
병렬 처리 환경 | 순차적으로 증가하는 값을 만들기 어려움 |
dbt의 replace 전략 | IDENTITY 초기화, SEQUENCE 부작용 등 충돌 발생 |
유지보수 어려움 | key 값 일관성 보장과 downstream 영향 관리가 매우 복잡 |
키 조합 vs Surrogate Key에 대해서
항목 | 키 조합 (Natural Key) | Surrogate Key (추천) |
조인 성능 | 느림 | 빠름 |
컬럼 길이 | 복합키 → 길어짐 | 단일 정수형 |
확장성 | 낮음 (SCD 관리 어려움) | 높음 |
데이터 변경 대응 | 취약 | 유연 |
Snowflake SEQUENCE 기반 Surrogate Key 자동화 매크로 (MIISK 방식)
📌 목적:
- Snowflake의 SEQUENCE 객체를 이용하여 정수형 surrogate key 자동 생성
📊 구성 도식:
┌───────────────────────┐
│ dbt_project.yml │
│ meta.surrogate_key: T │
└────────┬──────────────┘
▼
┌────────────────────────────┐
│ macros/generate_sequences.sql │ ← on-run-start
└────────────────────────────┘
▼
┌───────────────────────────┐
│ 모델 내 {{ increment_sequence() }} 사용 │
└───────────────────────────┘
매크로 예제
generate_sequences.sql
{% macro generate_sequences() %}
{% if execute %}
{% set models = graph.nodes.values() | selectattr('resource_type', 'eq', 'model') %}
{% set sk_models = [] %}
{% for model in models %}
{% if model.config.meta.surrogate_key %}
{% do sk_models.append(model) %}
{% endif %}
{% endfor %}
{% endif %}
{% for model in sk_models %}
{% if flags.FULL_REFRESH or model.config.materialized == 'table' %}
create or replace sequence {{ model.database }}.{{ model.schema }}.{{ model.name }}_seq;
{% else %}
create sequence if not exists {{ model.database }}.{{ model.schema }}.{{ model.name }}_seq;
{% endif %}
{% endfor %}
{% endmacro %}
increment_sequence.sql
{% macro increment_sequence() %}
{{ this.name }}_seq.nextval
{% endmacro %}
dim_customers.sql
select
{{ increment_sequence() }} as customer_id,
first_name,
last_name
from source_customers
해시 기반 Surrogate Key (dbt_utils 활용)
목적
- 여러 column을 조합한 해시 기반 surrogate key 생성
- 플랫폼 독립적, 멱등성(idempotent) 보장
도식
┌──────────────────────────────┐
│ dbt_utils.generate_surrogate_key() │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ md5(concat_ws('||', col1, col2, ...)) │
└──────────────────────────────┘
사용 예시
select
{{ dbt_utils.generate_surrogate_key(['user_id', 'event_date']) }} as user_event_sk,
user_id,
event_date,
...
from {{ ref('stg_events') }}
내부적으로는 다음과 유사
md5(cast(user_id as string) || '||' || cast(event_date as string))
SCD Type 2 적용 예시 (surrogate key + hash 활용)
📌 목적
- 변경 이력을 저장하기 위한 SCD Type 2 전략
- surrogate_key, is_current, start_date, end_date 포함
📊 도식:
Input (stg_customers)
┌────────┬───────────┐
│ cust_id│ name │
├────────┼───────────┤
│ 101 │ Alice │
│ 101 │ Alice Kim │ ← 변경
└────────┴───────────┘
SCD 적용 후 (dim_customers)
┌─────────────┬───────────┬────────────┬────────────┬────────────┐
│ surrogate_sk│ cust_id │ name │ start_date │ end_date │
├─────────────┼───────────┼────────────┼────────────┼────────────┤
│ 1 │ 101 │ Alice │ 2022-01-01 │ 2023-06-01 │
│ 2 │ 101 │ Alice Kim │ 2023-06-02 │ 9999-12-31 │
└─────────────┴───────────┴────────────┴────────────┴────────────┘
🧩 dbt 모델 예시 (dim_customers_scd2.sql)
{{ config(
materialized='incremental',
unique_key='surrogate_sk'
) }}
with source as (
select *,
{{ dbt_utils.generate_surrogate_key(['cust_id', 'name']) }} as natural_sk
from {{ ref('stg_customers') }}
),
deduped as (
select *,
row_number() over (partition by cust_id order by updated_at desc) as row_num
from source
)
select
{{ dbt_utils.generate_surrogate_key(['cust_id', 'name']) }} as surrogate_sk,
cust_id,
name,
current_timestamp() as start_date,
'9999-12-31' as end_date,
true as is_current
from deduped
where row_num = 1
최종 요약
🎯 결론: 언제 무엇을 써야 하나?
상황 | 추천 방식 |
내부 분석/BI 중심, 자동화 지향 | ✅ Hash 기반 Surrogate Key |
외부 시스템(PK)과 연동 필요 | ⚠️ MIISK 방식 (정수형) |
기존 DB에 Surrogate Key 존재 | 🔁 해당 값을 그대로 사용 |
매우 큰 데이터셋, 성능 최우선 | ⚠️ 정수형 고려 가능 (단, 관리 복잡) |
dbt에서는 기본적으로 해시 기반 surrogate key 생성 방식을 사용하며, 이는 멱등성, 자동화, 유지보수 관점에서 가장 권장되는 방식입니다. Snowflake SEQUENCE 기반의 MIISK 방식도 가능하지만, 운영/성능/순서 보장이 필요한 상황이 아니라면 일반적으로는 해시 방식이 더 적합합니다.
참고
‣
‣
Share article