2024. 3. 1. 23:02ㆍTIL
dbt - Database Normalization
ETL을 하는 이유는 결국 ELT를 하기 위함이며 이때 데이터 품질 검증이 중요하다. 입출력 체크, 다양한 품질 검사, 리니지 체크, 데이터 히스토리 파악을 통해 비용/노력 감소와 생산성 증대를 할 수 있다. Database Normalization는 데이터베이스를 좀 더 조직적이고 일관된 방법으로 디자인하려는 방법이다. 데이터베이스 정합성을 쉽게 유지하고 레코드들을 수정/적재/삭제를 용이하게 한다. Normalization의 종류는 다음과 같다.
- 1NF (First Normal Form)
- 한 셀에는 하나의 값만 있어야 함 (atomicity)
- Primary Key가 있어야함
- 중복된 키나 레코드들이 없어야 함
- 2NF (Second Normal Form)
- 일단 1NF를 만족해야 함
- 다음으로 Primary Key를 중심으로 의존결과를 알 수 있어야 함
- 부분적인 의존도가 없어야 함
- 모든 부가 속성들은 Primary key를 가지고 찾을 수 있어야 함
- 3NF (Third Normal Form)
- 일단 2NF를 만족해야 함
- 전이적 부분 종속성을 없어야 함
DW나 DL에서는 모든 테이블들의 히스토리를 유지하는 것이 중요하다. 보통 두 개의 timestamp 필드인 created_at (생성시간으로 한번 만들어지면 고정됨)와 updated_at (마지막 수정 시간을 나타냄)를 갖는 것이 좋다. 이러한 정보를 가진 테이블을 Slowly Changing Dimensions이라 한다. 이 경우 컬럼의 성격에 따라 어떻게 유지할지 방법이 달라진다.
- SCD Type 0
- 한번 쓰고 나면 바꿀 이유가 없는 경우들
- 한번 정해지면 갱신되지 않고 고정되는 필드들
- 예) 고객 테이블이라면 회원 등록일, 제품 첫 구매일
- SCD Type 1
- 데이터가 새로 생기면 덮어쓰면 되는 컬럼들
- 처음 레코드 생성 시에는 존재하지 않았지만 나중에 생기면서 채우는 경우
- 예) 고객 테이블이라면 연간소득 필드
- SCD Type 2
- 특정 entity에 대한 데이터가 새로운 레코드로 추가되어야 하는 경우
- 예) 고객 테이블에서 고객의 등급 변화
- tier라는 컬럼의 값이 “regular”에서 “vip”로 변화하는 경우 변경시간도 같이 추가되어야 함
- SCD Type 3
- SCD Type 2의 대안으로 특정 entity 데이터가 새로운 컬럼으로 추가되는 경우
- 예) 고객 테이블에서 tier라는 컬럼의 값이 “regular”에서 “vip”로 변화하는 경우
- previous_tier라는 컬럼 생성
- 변경시간도 별도 컬럼으로 존재해야 함
- SCD Type 4
- 특정 entity에 대한 데이터를 새로운 Dimension 테이블에 저장하는 경우
- SCD Type 2의 변종
- 예) 별도의 테이블로 저장하고 이 경우 아예 일반화할 수도 있음
dbt (Data Bulid Tool) 사용 시나리오
dbt (Data Bulid Tool)은 ELT용 오픈소스로 dbt Labs라는 회사가 상용화하였다. 이로 인해 Analytics Engineer라는 직군이 생겼으며 다양한 데이터 웨어하우스(Redshift, Snowflake, Bigquery, Spark)를 지원하고 클라우드 버전도 존재한다. dbt 구성 컴포넌트는 데이터 모델, 데이터 품질 검증, 스냅샷이 있다. 데이터 모델은 테이블들을 몇 개의 티어로 관리한다. dbt를 사용하는 이유는 다음과 같다.
- 데이터 변경 사항을 이해하기 쉽고 필요하다면 롤백 가능
- 데이터 간 리니지 확인 가능
- 데이터 품질 테스트 및 에러 보고
- Fact 테이블의 증분 로드 (Incremental Update)
- Dimension 테이블 변경 추적 (히스토리 테이블)
- 용이한 문서 작성
Redshift를 사용하여 A/B 테스트 분석을 쉽게 하기 위한 ELT 테이블을 만들어 볼 것이다. 입력 테이블은 user_event, user_variant, user_metadata이고 생성 테이블은 Variant별 사용자별 일별 요약 테이블(variant_id, user_id, datestamp, age, gender, 총 impression, 총 click, 총 purchase, 총 revenue)이다. Production DB에 저장되는 정보들을 Data Warehouse로 적재했다고 가정한다. 입력 데이터들은 다음과 같다.
- raw_data.user_event
- 사용자/날짜/아이템별로 impression이 있는 경우 그 정보를 기록하고 impression으로부터 클릭, 구매, 구매 시 금액을 기록. 실제 환경에서는 이런 aggregate 정보를 로그 파일 등의 소스(하나 이상의 소스가 될 수도 있음)로부터 만들어내는 프로세스가 필요함
CREATE TABLE raw_data.user_event (
user_id int,
datestamp timestamp,
item_id int,
clicked int,
purchased int,
paidamount int
);
- raw_data.user_variant
- 사용자가 소속한 AB test variant를 기록한 파일 (control vs. test)
CREATE TABLE raw_data.user_variant (
user_id int,
variant_id varchar(32) -- control vs. test
);
- raw_data.user_metadata
- 사용자에 관한 메타 정보가 기록된 파일 (성별, 나이 등등)
CREATE TABLE raw_data.user_metadata (
user_id int,
age varchar(16),
gender varchar(16),
updated_at timestamp
);
입력 데이터를 Fact 테이블과 Dimension 테이블로 구분할 수 있다. user_event 테이블은 Fact 테이블, user_variant, user_metadata은 Dimension 테이블이라 할 수 있다.
- Fact 테이블: 분석의 초점이 되는 양적 정보를 포함하는 중앙 테이블
- 일반적으로 매출 수익, 판매량, 이익과 같은 측정 항목 포함. 비즈니스 결정에 사용
- Fact 테이블은 일반적으로 외래 키를 통해 여러 Dimension 테이블과 연결됨
- 보통 Fact 테이블의 크기가 훨씬 더 큼
- Dimension 테이블: Fact 테이블에 대한 상세 정보를 제공하는 테이블
- 고객, 제품과 같은 테이블로 Fact 테이블에 대한 상세 정보 제공
- Fact 테이블의 데이터에 맥락을 제공하여 다양한 방식으로 분석 가능하게 해 줌
- Dimension 테이블은 primary key를 가지며, fact 테이블에서 참조 (foreign key)
- 보통 Dimension 테이블의 크기는 훨씬 더 작음
최종 생성 데이터(ELT 테이블)를 SELECT로 표현하면 다음과 같다.
SELECT
variant_id,
ue.user_id,
datestamp,
age,
gender,
COUNT(DISTINCT item_id) num_of_items, -- 총 impression
COUNT(DISTINCT CASE WHEN clicked THEN item_id END) num_of_clicks, -- 총 click
SUM(purchased) num_of_purchases, -- 총 purchase
SUM(paidamount) revenue -- 총 revenue
FROM raw_data.user_event ue
JOIN raw_data.user_variant uv ON ue.user_id = uv.user_id
JOIN raw_data.user_metadata um ON uv.user_id = um.user_id
GROUP by 1, 2, 3, 4, 5;
dbt 설치와 환경 설정
dbt 사용절차는 다음과 같다.
- dbt 설치
- dbt Cloud vs. dbt Core
- git을 보통 사용함
- dbt 환경설정
- Connector 설정
- Connector가 바로 바탕이 되는 데이터 시스템 (Redshift, Spark, …)
- 데이터 모델링 (tier)
- Raw Data -> Staging -> Core
- 테스트 코드 작성
- (필요하다면) Snapshot 설정
dbt Models : Input
Model이란 ELT 테이블을 만듬에 있어 기본이 되는 빌딩블록이다. Model은 테이블, View, CTE 형태로 존재하며 입력, 중간, 최종 테이블을 정의하는 곳이다. View는 SELECT 결과를 기반으로 만들어진 가상 테이블로 기존 테이블의 일부 혹은 여러 테이블들을 조인한 결과를 제공한다. View는 사용자가 원본 데이터에 접근하지 않아도 필요 데이터에 직접 접근할 수 있게 해주고 복잡한 쿼리를 간소화한다. 하지만 매번 쿼리가 실행되므로 시간이 걸릴 수 있고 원본 데이터의 변경을 모르면 실행이 실패한다. CTE(Common Table Expression)는 WITH절에 정의된 명명된 하위 쿼리이다. Model 구성 요소는 다음과 같다.
- Input
- 입력(raw)과 중간(staging, src) 데이터 정의
- raw는 CTE로 정의
- staging은 View로 정의
- Output
- 최종(core) 데이터 정의
- core는 Table로 정의
- 이 모두는 models 폴더 밑에 sql 파일로 존재
- 기본적으로는 SELECT + Jinja 템플릿과 매크로
- 다른 테이블들을 사용 가능 (reference)
- 이를 통해 리니지 파악

dbt Models : Output
Materialization이란 입력 데이터(테이블)들을 연결해서 새로운 데이터(테이블) 생성하는 것이다. 보통 여기서 추가 transformation이나 데이터 클린업을 수행한다. Materialization 종류는 다음과 같다.
- View
- 데이터를 자주 사용하지 않는 경우
- Table
- 데이터를 반복해서 자주 사용하는 경우
- Incremental (Table Appends)
- Fact 테이블
- 과거 레코드를 수정할 필요가 없는 경우
- Ephemeral (CTE)
- 한 SELECT에서 자주 사용되는 데이터를 모듈화하는데 사용
Jinja 템플릿이란 파이썬이 제공해주는 템플릿 엔진으로 Flask에서 많이 사용한다. 입력 파라미터 기준으로 HTML 페이지(마크업)를 동적으로 생성하고 조건문, 루프, 필터등을 제공한다.

dbt Seeds
많은 dimension 테이블들은 크기가 작고 많이 변하지 않는다. Seeds는 이를 파일 형태로 데이터웨어하우스로 로드하는 방법이다. Seeds는 작은 파일 데이터(보통 csv 파일)를 지칭한다.
dbt Sources
Staging 테이블을 만들 때 입력 테이블들이 자주 바뀐다면 models 밑의 .sql 파일들을 일일이 찾아 바꿔주어야 하는 번거로움이 있다. 이런 번거로움을 해결하기 위한 것이 Sources이다. Sources는 입력 테이블에 별칭을 주고 별칭을 staging 테이블에서 사용한다. Sources는 기본적으로 처음 입력이 되는 ETL 테이블을 대상으로 하고 별칭 제공, 최신 레코드 체크 기능을 제공한다. 테이블 이름에 별명(alias)을 주는 것을 통해 ETL단의 소스 테이블이 바뀌어도 뒤에 영향을 주지 않는다. 그리고 추상화를 통한 변경처리를 용이하게 한다. Source 테이블들에 새 레코드가 있는지 체크해주는 기능도 제공한다. Sources 최신성(Freshness)은 특정 데이터가 소스와 비교해서 얼마나 최신성이 떨어지는지 체크하는 기능이다.
dbt Snapshots
Dimension 테이블은 성격에 따라 변경이 자주 생길 수 있다. dbt에서는 테이블의 변화를 계속적으로 기록함으로써 과거 어느 시점이건 다시 돌아가서 테이블의 내용을 볼 수 있는 기능을 Snapshots이라 한다. 이를 통해 테이블에 문제가 있을 경우 과거 데이터로 롤백 가능하고 다양한 데이터 관련 문제 디버깅도 쉬워진다.
SCD Type 2로 Dimension 테이블에서 특정 entity에 대한 데이터가 변경되는 경우에 변경시간도 같이 추가되어야 하는데 이때 새로운 Dimension 테이블(history/snapshot 테이블)을 생성한다.
dbt의 스냅샷 처리 방법은 먼저 snapshots 폴더에 환경설정이 된다. snapshots을 하려면 데이터 소스는 Primary key가 존재해야 하고 레코드의 변경시간을 나타내는 타임스탬프 필요(updated_at, modified_at 등등)한 조건을 만족해야 한다. 변경 감지 기준은 Primary key 기준으로 변경시간이 현재 DW에 있는 시간보다 미래인 경우이다. Snapshots 테이블에는 총 4개의 타임스탬프( dbt_scd_id, dbt_updated_at, valid_from, valid_to)가 존재한다.
dbt Tests, Documentation, Expectations
Tests는 데이터 품질을 테스트하는 방법으로 두 가지가 존재한다. 내장 일반 테스트(“Generic”)는 unique, not_null, accepted_values, relationships 등의 테스트를 지원하며 models 폴더에서 만든다. 커스텀 테스트(“Singular”)는 기본적으로 SELECT로 간단하며 결과가 리턴되면 “실패”로 간주하며 tests 폴더에서 만든다.
dbt Documentation는 문서와 소스 코드를 최대한 가깝게 배치하려는 취지로 만들어졌다. 문서화 자체는 기존 .yml 파일에 문서화 추가(선호되는 방식)하는 방법과 독립적인 markdown 파일 생성하는 방법이 존재한다. 이를 경량 웹서버로 서빙하며 overview.md가 기본 홈페이지가 되고 이미지 등의 asset 추가도 가능하다.
dbt Expectations는 Great Expectations에서 영감을 받아 dbt용으로 만든 dbt 확장판이다. 설치 후 packages.yml에 등록해서 앞서 dbt 제공 테스트들과 같이 사용한다.
'TIL' 카테고리의 다른 글
| AI 발전 동향 이해 (1) | 2024.03.04 |
|---|---|
| A/B 테스트 분석 시각화 (0) | 2024.03.03 |
| 가상 데이터 기반 A/B 테스트 분석 (0) | 2024.02.29 |
| A/B Test 관련 통계 살펴보기 (1) | 2024.02.28 |
| A/B 테스트 과정 살펴보기 (0) | 2024.02.28 |