LLM WikiAccess-protected knowledge portal
← 스터디 홈
1편 · 약 24분

분석 모델링 기본: grain, fact, dimension, slowly changing dimension

분석 모델링이란 무엇인가

OLTP 데이터베이스는 트랜잭션을 빠르고 안전하게 처리하도록 설계된다. 인덱스는 단건 조회를 최적화하고, 정규화는 중복을 제거해 갱신 이상을 막는다. 이 구조는 "오늘 주문한 고객이 누구인가"를 빠르게 답하지만, "지난 3년간 카테고리별 월평균 주문 금액이 어떻게 바뀌었는가"를 묻는 순간 수십 개 테이블을 조인해야 하고 응답이 느려진다.

분석 모델링(dimensional modeling)은 이 반대의 목표로 출발한다. 데이터를 분석가가 질문하는 방식으로 구조화해, 집계·필터·드릴다운이 자연스럽게 흐르도록 만드는 것이다. 1996년 Ralph Kimball이 체계화한 이 방법론은 30년 가까이 지나도록 데이터 웨어하우스 설계의 기본 언어로 남아 있다.

분석 모델링의 핵심 구성 요소는 세 가지다. grain(사실 테이블 한 행이 나타내는 정밀도), fact table(측정값을 담는 테이블), dimension table(맥락을 담는 테이블). 여기에 시간이 지나면서 차원 데이터가 변할 때 이를 다루는 slowly changing dimension(SCD) 패턴이 더해진다.

Grain
사실 테이블 한 행이
나타내는 정밀도 정의
가장 먼저, 가장 명확하게
선언해야 하는 결정
Fact Table
숫자 측정값(measure) 저장
트랜잭션·스냅샷·누적
집계의 대상이 되는
비즈니스 이벤트
Dimension Table
비즈니스 맥락(context) 저장
필터·그룹화의 기준
누가·언제·어디서·무엇을
설명하는 서술형 데이터
SCD
차원 데이터 변경 이력
처리 전략 (Type 1~6)
과거 기준 분석 정확성을
좌우하는 핵심 결정
분석 모델링의 네 핵심 요소

1. Grain: 가장 먼저 선언하고 가장 엄격하게 지켜야 하는 결정

Kimball 방법론에서 grain은 "사실 테이블 한 행이 정확히 무엇을 나타내는가"에 대한 선언이다. Kimball은 이를 "grain 선언은 설계의 토대이며, grain이 결정되기 전에 다음 단계로 넘어가서는 안 된다"고 명시했다.

1.1 Grain을 정의하는 방법

Grain은 비즈니스 언어로 표현해야 한다. 기술적인 표현("primary key별 행")이 아니라 실제 업무 이벤트로 기술하는 것이다.

잘못된 grain 선언올바른 grain 선언
"주문 관련 데이터""개별 주문 라인 아이템 1건"
"사용자 활동""사용자 1명이 1번 세션 중 1개 페이지를 방문한 이벤트"
"매출 요약""특정 매장의 특정 날짜 일별 매출 합계"

grain이 "개별 주문 라인 아이템"이라면, 한 주문에 상품 3개가 담겨 있을 때 사실 테이블에 3행이 생성된다. grain이 "주문 헤더"라면 1행이 생성된다. 이 차이가 나중에 "상품별 매출 분석"을 할 수 있는지 없는지를 결정한다.

1.2 Grain을 잘못 설정했을 때 생기는 문제

가장 흔한 실수는 grain 혼합이다. 라인 아이템 수준의 행과 주문 헤더 수준의 요약이 같은 사실 테이블에 섞이는 경우다. 이렇게 되면 SUM(amount)를 계산할 때 중복 집계가 발생하고, 어떤 집계가 올바른지 판단하기 어렵다.

또 다른 실수는 너무 거친 grain이다. 예를 들어 grain을 "일별 상품 카테고리 매출 합계"로 정하면 개별 상품 분석이 불가능하다. 반대로 너무 세밀한 grain은 테이블 크기를 필요 이상으로 키우고 집계 쿼리를 느리게 한다.

실무에서는 가능한 가장 세밀한 atomic grain을 사용하는 것이 권고된다. 세밀한 grain에서는 집계로 거친 수준을 만들 수 있지만, 거친 grain에서는 세밀한 분석이 불가능하기 때문이다.


2. Fact Table: 측정값을 담는 비즈니스 이벤트의 기록

사실 테이블은 비즈니스 이벤트에서 발생하는 숫자 측정값(measure)을 저장한다. 각 행은 정의된 grain에 맞는 이벤트 하나를 나타내고, 해당 이벤트의 차원 키(FK)와 측정값을 포함한다.

2.1 세 가지 사실 테이블 유형

Kimball은 사실 테이블을 세 가지 유형으로 구분했다.

① 트랜잭션 사실 테이블(Transaction Fact Table)

가장 일반적인 유형이다. 비즈니스 이벤트가 발생할 때마다 행이 추가된다. 한번 기록된 행은 변경되지 않는다.

-- 트랜잭션 사실 테이블 예시: 주문 라인 아이템
CREATE TABLE fct_order_items (
  order_item_sk  BIGINT     -- surrogate key (PK)
  date_key       INT        -- FK → dim_date
  customer_key   INT        -- FK → dim_customer
  product_key    INT        -- FK → dim_product
  store_key      INT        -- FK → dim_store
  quantity       INT        -- measure: 주문 수량
  unit_price     DECIMAL    -- measure: 단가
  discount_amount DECIMAL   -- measure: 할인액
  net_amount     DECIMAL    -- measure: 실 결제액 (quantity × unit_price - discount)
);

특징: 행이 누적만 되고 수정되지 않으므로 append-only 파티션 테이블로 효율적으로 관리할 수 있다.

② 주기적 스냅샷 사실 테이블(Periodic Snapshot Fact Table)

특정 시점의 상태를 주기적으로 기록한다. 매일 자정 재고 수준이나 계좌 잔액처럼 "특정 시점에 측정된 상태"가 필요할 때 사용한다.

-- 주기적 스냅샷: 일별 계좌 잔액
CREATE TABLE fct_account_balance_daily (
  snapshot_date_key INT     -- FK → dim_date (기준 날짜)
  account_key       INT     -- FK → dim_account
  ending_balance    DECIMAL -- 당일 마감 잔액 (semi-additive)
  transaction_count INT     -- 당일 거래 건수 (additive)
);

주의: 해당 기간에 활동이 없어도 "변화 없음" 행이 매일 생성된다. 이 특성 때문에 테이블 크기가 크게 증가할 수 있다.

③ 누적 스냅샷 사실 테이블(Accumulating Snapshot Fact Table)

하나의 비즈니스 프로세스가 여러 단계를 거쳐 완료될 때 사용한다. 주문이 결제 → 포장 → 배송 → 수령 단계를 거치는 경우처럼, 프로세스가 진행될수록 동일한 행이 갱신된다.

-- 누적 스냅샷: 주문 이행 파이프라인
CREATE TABLE fct_order_fulfillment (
  order_key         INT
  customer_key      INT
  placed_date_key   INT      -- 주문일
  paid_date_key     INT      -- 결제 완료일
  packed_date_key   INT      -- 포장 완료일
  shipped_date_key  INT      -- 발송일
  delivered_date_key INT     -- 수령일
  placed_to_shipped_days INT -- 파생 지표: 발송까지 소요일
  order_amount      DECIMAL
);

단계가 완료되면 해당 날짜 키가 채워지고 행이 UPDATE된다. 각 단계 사이의 소요 시간을 쉽게 분석할 수 있다는 게 이 유형의 강점이다.

2.2 측정값(Measure)의 세 종류

종류정의예시주의
Additive모든 차원에서 합산 가능판매 금액, 수량, 비용없음
Semi-additive일부 차원에서만 합산 가능계좌 잔액, 재고 수준날짜 기준 SUM 금지
Non-additive어떤 차원으로도 합산 불가비율(%), 단가집계 후 계산 필요

Semi-additive의 대표 예는 계좌 잔액이다. A 계좌 잔액 1,000과 B 계좌 잔액 2,000을 더해 "총 잔액 3,000"을 계산하는 것은 의미 있다. 하지만 1월 1일 잔액과 1월 2일 잔액을 더한 값은 비즈니스적 의미가 없다. 따라서 날짜 차원으로는 SUM이 아니라 MAX, LAST_VALUE, AVG 같은 함수를 써야 한다.


3. Dimension Table: 분석의 맥락을 제공하는 서술형 데이터

차원 테이블은 사실 테이블의 측정값에 비즈니스 맥락을 부여한다. "얼마나 팔렸는가"는 사실 테이블에 있지만, "어느 지역에서, 어느 카테고리 상품이, 어떤 고객층에게 팔렸는가"는 차원 테이블이 답한다.

fct_order_items date_key (FK) customer_key (FK) product_key (FK) store_key (FK) quantity net_amount ← measures → dim_date date_key (PK) year, quarter, month weekday, is_holiday dim_customer customer_key (PK) name, email city, tier, signup_date ← SCD Type 2 → dim_product product_key (PK) name, sku, brand category, subcategory cost_price, launch_date dim_store store_key (PK) name, city, region store_type, open_date
스타 스키마: 사실 테이블과 차원 테이블의 관계

3.1 차원 테이블의 구조

차원 테이블은 일반적으로 하나의 surrogate key(대리키)를 기본 키로 가진다. 원본 시스템의 자연 키(natural key)와 구분되는 이 대리키는 정수형으로 단순하게 생성되고, SCD Type 2처럼 동일 엔티티의 여러 버전을 구분할 때 필수적이다.

차원 속성은 두 종류로 나뉜다. 계층적 속성(hierarchical attribute)은 city → state → country처럼 드릴다운 분석의 경로를 형성한다. 서술적 속성(descriptive attribute)은 고객 등급, 상품 색상, 매장 유형처럼 필터링과 레이블링에 쓰인다.

3.2 Conformed Dimension(공유 차원)

여러 사실 테이블이 동일한 차원 테이블을 공유하는 설계를 Conformed Dimension이라고 한다. dim_date가 가장 대표적이다. 주문 사실 테이블과 반품 사실 테이블이 동일한 dim_date를 참조하면, "같은 날짜 기준으로 주문과 반품을 함께 분석"하는 cross-fact 쿼리가 가능해진다.

Conformed dimension은 조직 전체가 동일한 정의를 쓴다는 의미이기도 하다. "고객"의 정의가 팀마다 다르면 conformed dimension 자체가 성립하지 않는다. 이것이 dimension 설계가 기술적 결정인 동시에 비즈니스 합의 과정인 이유다.


4. Slowly Changing Dimension: 변하는 차원을 어떻게 다룰 것인가

비즈니스 세계에서 차원 데이터는 변한다. 고객의 등급이 바뀌고, 직원이 부서를 이동하고, 상품 카테고리가 재분류된다. 이 변경을 어떻게 처리하느냐에 따라 과거 데이터 분석의 정확성이 달라진다.

"3년 전 VIP 등급이었던 고객의 구매 패턴"을 분석하려면, 그 당시 그 고객이 VIP였다는 사실이 데이터에 남아 있어야 한다. 변경을 단순히 덮어쓰면 이 분석은 불가능하다.

4.1 SCD Type 0: 변경을 허용하지 않음

속성이 처음 설정된 이후 절대 바뀌지 않는 경우다. 고객의 최초 가입 채널이나 생년월일처럼 비즈니스적으로 불변해야 하는 데이터에 적용한다. 원본에서 값이 바뀌더라도 무시한다.

4.2 SCD Type 1: 덮어쓰기

이전 값을 새 값으로 덮어쓴다. 이력은 남지 않는다. 오류 수정(이름 오타 수정, 주소 오타 수정)이나 비즈니스적으로 이력이 필요 없는 경우에 적합하다.

-- SCD Type 1: UPDATE로 처리
UPDATE dim_customer
SET email = '[email protected]'
WHERE customer_key = 12345;

장점: 단순하고 테이블이 작게 유지된다. 단점: 변경 이전 상태로 과거를 분석할 수 없다.

4.3 SCD Type 2: 새 행 추가 (가장 많이 쓰임)

변경 전: 고객 등급 SILVER
customer_key: 1001
customer_id: C-500 (NK)
tier: SILVER
valid_from: 2023-01-01
valid_to: 9999-12-31
is_current: TRUE
↓ 2025-06-01 등급 GOLD로 변경
변경 후: 두 행이 공존
customer_key: 1001
customer_id: C-500
tier: SILVER
valid_from: 2023-01-01
valid_to: 2025-05-31
is_current: FALSE
customer_key: 1002 (신규 SK)
customer_id: C-500
tier: GOLD
valid_from: 2025-06-01
valid_to: 9999-12-31
is_current: TRUE
사실 테이블은 주문 당시의 customer_key를 저장 → 과거 주문은 1001(SILVER)에, 이후 주문은 1002(GOLD)에 연결됨
SCD Type 2: 이력 행 추가 패턴

변경이 발생하면 기존 행의 valid_to를 업데이트하고 is_current = FALSE로 표시한 뒤, 새 값을 가진 새 행을 삽입한다. 사실 테이블은 이벤트 발생 시점의 customer_key를 저장하므로, 나중에 "그 당시 그 고객의 등급"을 정확하게 조회할 수 있다.

장점: 완전한 이력 보존, 특정 시점 분석 가능. 단점: 테이블이 커지고, 현재 값 조회 시 is_current = TRUE 필터가 필요하다.

실무에서는 valid_from / valid_to / is_current 세 컬럼을 표준으로 쓰거나, 일부 시스템에서는 valid_from만 관리하고 현재 행을 판별하는 방식을 쓰기도 한다.

4.4 SCD Type 3: 컬럼 추가 (제한적 이력)

새 컬럼을 추가해 이전 값과 현재 값을 나란히 저장한다.

-- SCD Type 3: 이전·현재 값을 컬럼으로 보관
ALTER TABLE dim_customer ADD COLUMN previous_tier VARCHAR(20);

-- 변경 시
UPDATE dim_customer
SET previous_tier = tier,
    tier = 'GOLD',
    tier_changed_at = '2025-06-01'
WHERE customer_key = 1001;

장점: 구조가 단순하고 현재/이전 비교 쿼리가 쉽다. 단점: 두 번 이상의 변경 이력을 저장할 수 없다. 변경 횟수가 정해진 경우(예: "이전 부서"만 추적하면 되는 경우)에 적합하다.

4.5 SCD Type 4: 이력 전용 테이블 분리

현재 값은 기본 차원 테이블에, 이력은 별도 히스토리 테이블에 저장한다.

dim_customer          → 현재 상태만 저장 (빠른 조회)
dim_customer_history  → 변경 이력 전체 보존 (이력 분석)

장점: 현재 값 조회가 빠르고 기본 테이블이 작다. 단점: 이력 분석 시 두 테이블을 조인해야 한다.

4.6 SCD Type 6: Hybrid (Type 1 + Type 2 + Type 3)

Type 2처럼 새 행을 추가하면서, 동시에 모든 행에 현재 값을 Type 1처럼 채워 넣는 방식이다.

customer_key | tier (현재값, Type1) | tier_at_time (당시값, Type2) | is_current
-------------|---------------------|-------------------------------|----------
1001         | GOLD                | SILVER                        | FALSE
1002         | GOLD                | GOLD                          | TRUE

"현재 값 기준으로 전체 이력 집계" + "당시 값 기준으로 특정 시점 분석"을 모두 지원한다. 복잡하지만 분석 유연성이 높다.

4.7 SCD 유형 선택 기준

상황권고 유형
이력이 비즈니스적으로 무의미 (오타 수정 등)Type 1
완전한 이력 보존이 필요한 핵심 차원 (고객, 직원)Type 2
"이전 값"만 알면 충분하고 변경 횟수가 적음Type 3
현재 조회 속도가 중요하고 이력은 드물게 사용Type 4
현재 기준 집계 + 시점 기준 분석 둘 다 필요Type 6

5. 실무에서 자주 저지르는 설계 실수

① Grain을 명시하지 않고 시작하는 것. 팀원마다 grain을 다르게 이해하면 사실 테이블에 grain이 혼합되고, 집계 결과가 맞지 않는다.

② 사실 테이블에 텍스트 서술형 컬럼을 넣는 것. 상품 이름, 고객 이름처럼 분석의 맥락이 되는 데이터는 차원 테이블에 있어야 한다. 사실 테이블에 두면 중복이 발생하고 업데이트가 어렵다.

③ SCD를 고려하지 않고 자연 키를 FK로 사용하는 것. 고객의 등급이 변해도 사실 테이블의 FK가 같은 자연 키를 가리키면, "주문 당시 고객 등급"이 아니라 "현재 고객 등급"으로 분석된다.

④ 모든 차원에 SCD Type 2를 적용하는 것. 이력이 필요 없는 차원에도 Type 2를 쓰면 테이블이 불필요하게 커지고 쿼리가 복잡해진다.

⑤ Non-additive measure를 SUM으로 집계하는 것. 단가나 비율을 SUM으로 더하면 의미 없는 숫자가 나온다.


마무리

분석 모델링의 네 핵심 요소—grain, fact table, dimension table, SCD—는 서로 독립적인 개념이 아니다. Grain이 잘못 설정되면 사실 테이블의 집계가 틀리고, SCD를 고려하지 않으면 차원 테이블이 과거 분석을 왜곡한다.

Kimball 방법론이 30년간 유효한 이유는 이 구조가 비즈니스 질문의 방식(누가, 언제, 어디서, 무엇을, 얼마나)과 자연스럽게 대응하기 때문이다. 다음 편에서는 이 요소들을 조합하는 스키마 설계 전략—Star, Snowflake, Wide table—과 각 접근법의 실제 트레이드오프를 살펴본다.

References

  • Ralph Kimball, Margy Ross, The Data Warehouse Toolkit: The Definitive Guide to Dimensional Modeling (3rd ed., 2013): https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/books/data-warehouse-dw-toolkit/
  • Kimball Group, "The 10 Essential Rules of Dimensional Modeling": https://www.kimballgroup.com/2009/05/the-10-essential-rules-of-dimensional-modeling/
  • dbt Labs, "Dimensional Modeling": https://docs.getdbt.com/terms/dimensional-modeling
  • Kimball Group, "Slowly Changing Dimensions": https://www.kimballgroup.com/2008/09/slowly-changing-dimensions/
  • Databricks Blog, "Dimensional Modeling in the Modern Data Warehouse": https://www.databricks.com/blog/2022/05/20/the-beginners-guide-to-the-data-lakehouse.html
  • Airbyte Blog, "Fact Tables vs. Dimension Tables": https://airbyte.com/data-engineering-resources/fact-tables-vs-dimension-tables
  • dbt Labs, "SCD Type 2 with dbt snapshots": https://docs.getdbt.com/docs/build/snapshots