- We are in it for the long haul!

 

- Now then, we will need to look up your application.

 

- Just follow your instinct and choose an island with a layout that looks appealing.

 

- I'm sorry, that was just a hypothetical question.

 

- The chartered flight for Deserted Island Gateway Package is ready for boarding.

 

- Any customers waiting to board should make their way to the gate at this time.

- Please make your way to the meeting room.

 

- Wanna get left behind?

 

- I will turn it over to you, Mr. Nook.

- I will pass it over to you, Mr. Nook.

 

- First things first.

 

- I think this spot will do the trick.

- A little oil will do the trick.

- Just a few minutes of heat will do the trick.

 

- You can find them scattered about the island.

- Autumn leaves were scattered about the garden.

 

- I suspect they are real apples

 

- No way I'm gonna let you show me up

A: I just finished the report ahead of schedule!

B: Nice, but no way I'm gonna let you show me up. I'll finish mine in ten minutes.

 

- I trust you had some success foraging for tree branches?

 

- Allow me to lighten your load.

 

- Our next task is to round up something to snack on.

 

 

 

 

 

- When you come across someone in trouble....

 

- Such kindness will surely find its way back to you!

 

- Sorry to place such a burden on you

- I’m sorry to put you through this trouble/inconvenience/such a terrible experience

 

- you will make a donation?

 

- we want you to have the honor.

 

- We'd like you to pick out the perfect location for the new shop!

 

- I guess that's it for my flimsy axe....

 

- It's the end of the line for my trusty flimsy shovel...

 

- I bet this island is about to become a whole lot livelier!

(lively - livelier - liveliest)

 

- We have got our work cut out for us if we are going to open by tomorrow

(have got = have)

 

- I can buy these from you for a total of 420 Bells.

 

- At long last, our museum has opened! This very day, no less!

(no less : 다름 아닌!)

 

- An impressive feat.

- Finishing the project on time with no budget was an impressive feat.

 

- The museum is open to the public free of admission.

 

- You will find there are entrances to the left, to the right, and into the basement.

 

- Each one leads to exhibit rooms wherein the donations collected thus far have been put on display.

(wherein : -한 곳에서)

 

- The donation process has been streamlined.

 

- I will happily oblige!

 

- By all means, do please take a look at our magnificent collections. It is surely a thing of wonder.

(By all means : 부디, 아무렴요/그럼요

"Can I borrow your pen?" / "By all means.")

 

- If any happen to find their way into your possession, I would be most eager to see them!

(happen to : 우연히 -하다

I happened to meet an old friend on the street.

I don't remember buying this book, but it somehow found its way into my possession. 우연히 갖게 된다는 뉘앙스)

 

- I can assist you!

 

- As it happens, these crop-eating pests use straw-like mouths to pierce plants and drink the juices.

 

- The spider is renowned for having eight eyes and eight legs...

 

- Some will eat creatures several tiems larger than themselves.

 

- They have also been known to ambush their victims, some even chase down their meals.

 

- Their long antennae and bulging eyes turn my stomach. Unappetizing indeed..


- what a wretched thing...

 

- This is making me feel rather woozy...

 

- rest assured the wretched thing will get the best of care here.

 

- It looks rather like the heads of snakes.

(꽤, 상당히 : It is rather cold today

차라리 : I would rather stay home than go out.

-보다, -대신 : I chose the blue one rather than the red one)

 

- You have a knack for finding valuable fossils.

 

- The cultural development in this island is a worthy endeavor indeed.

 

- In the meantime, I shall return these to you. Please think on what I've siad. The museum awaits.

 

- This is practically synonymous with the word "dinosaur" and for good reason!

 

- I am just relieved that I will never have to escape one.

 

- whether they are impulse snaps or brilliant compositions!

 

- I'm more of a dogfish person...

- I'm more of a tea person than a coffee person.

 

- They are all about getting emotions across.

- The teacher used pictures to get the idea across.

 

- We've been here for a little while, and it's really starting to come along.

- My English skills are coming along nicely.

 

- We are just overwhelmed that you even thought to come visit us!

 

- Something tells me that today is your lucky day.

- My gut tells me that today is your lucky day.

- I have a feeling that today is your lucky day.

 

- How about if I stop by every so often to sell some of our wares?

 

- In the black cabinet, with the tools and other miscellaneous goods.

 

- With our new shop come three new services!

 

- I've heard the turnip market is about to take off here on the island.

 

- We will buy any turnips you have on hand.

 

- ...And I think that wraps it up!

 

- Sorry, that explanation went on for a bit.

 

- You can use a slingshot to knock items out of the sky.

 

- You never know when you might want to bring something down.

 

- May as well you carry one around with you.

 

- ..But your first time is special! Enjoy these miles, courtesy of Nook Inc!

 

- I'm sorry to put you on hold.

- Could you please hold for a moment?

 

- For some time now I have been advertising the gateway pakcage...

(For some time now = For a while now, Lately, Recently)

 

 

- And wouldn't you know, out of the blue we have 3 interested parties!

 

- None of our applicants are too fond of the notion of sleeping in tents, you see.

 

- I'm afraid I got caught up in the excitement of persuading them to move here.

 

- In my hour of need... It most certainly must be fate.

 

- As if I have a choice...

- As if I have any other choice...

 

- With zero-interest financing and an extremely lax repayment plan, anything is possible!

 

- Now, regarding the aformentioned research...

 

- I fervently hope you will donate your precious finds to the cause.

 

- It seems to have specialized in eating plants up high.

 

- It likely needed to eat more or less every walking hour.

 

- I'd pay him a visit.

 

- I bet you were up to your ear holes in weeds when you first arrived.

 

- I can't say I have much in the way of news to report to you today.

- There wasn't much in the way of entertainment at the party.

- She doesn't have much in the way of experience.

 

- Now, where to begin...

 

- Makes it pretty easy to identify your blurry silhouette.

 

- I haven't gotten a reception this sweet and bubbly.

 

- I will leave the searching to you.

 

- The pieces are crammed back in here.

- My closet is crammed with old clothes.

- The subway was crammed with commuters.

 

- I promise to return the favor!

 

- No one knows who sends the gifts attached to balloons to our island.

 

- I was cleanin' my house and found this old DIY recipe I'd thought up.

- She thought up a brilliant plan to save money.

 

- No need to pull them all out!

 

- It has hooks on its feet that help it to cling tightly to trees.

 

- If you ask me, this is an insult to violins!

 

- The information in your Passport app can be used to spur conversation, so please make full use of it!

- A cup of coffee usually helps spur my creativity.

 

- Fireflies light up the night sky, taking my mind off the stresses of the day.

 

 

dbt 실습 repo

 

 

 

< dbt 란 >

 

SQL 기반 데이터 변환 도구.

ETL 의 Transformation 을 담당

 

dbt 로 만들어진 (데이터 처리를 위한) SQL은 재사용이 가능하고

dependency 관리가 수월하다는 것이 큰 장점임

또한 dbt 는 테스트 기능 역시 제공하며

문서화 및 배포까지 도와줌

 

dbt 는 데이터 모델링 작업을 위한 데이터 처리를 하는 데 많이 사용됨

 

 

 

 

 

 

< dbt 알아야 하는 개념들 >

 

  • //model
  • //ref()
  • //source()
  • //source freshness
  • //incremental model, table, view
  • //materialized : table, view
  • //macro
  • //jinja
  • //snapshot
  • //seed
  • dependency. dag graph 를 본다는 의미
  • test - schema test, singular test, custom test, whatnot
  • dbt core - 오픈소스, CLI 기반, 로컬 개발
  • dbt cloud - SaaS, UI 제공, 스케줄링, 협업 기능
  • 1. Incremental
    2. Snapshot
    3. Test
    4. Airflow + dbt 운영

 

 

 

 

먼저 jinja 에 대해 알아보자 

dbt 에서 자주 사용하는 sql 언어 포맷임

 

jinja SQL 을 동적으로 생성하기 위한 템플릿 언어

 

dbt 가 Jinja를 만나서 jinja 를 실행하면 : select * from {{ ref('stg_orders') }}
    ↓
jinja 위치에 실제 SQL 이 생성되어 교체됨 : select * from analytics.stg_orders
    ↓
그리고 warehouse 에서 실제 SQL이 실행됨(warehouse 는 jinja 해석 못 함. jinja 는 dbt 를 위한 것임)

 

 

 

1. {{}} : 표현식. 값을 출력할 때 사용함. {{}} 위치에 정해진 값이 변환되어 실행

예를 들어,

- {{ ref('stg_orders') }} 는 -> analytics.stg_orders 로 변환

- {{ target.schema }} 는  -> analytics_dev 로 변환

- {{ target.name }} 은 -> dev 혹은 prod 로 변환됨.

 

target.schema, target.name 은 profiles.yml 에 설정된 값을 가져오는 것

 

또한, dbt_project.yml 에 직접 넣은 값을 아래처럼 읽어올 수 있음

예를 들어 dbt_project.yml 에 아래 부분이 포함되어있다고 하자

vars:
  country: CA

 

모델.sql 내에서 다음과 같이 불러올 수 있음

select *
from customers

where country='{{ var("country") }}'

 

 

 

2. {% %} : 제어문. 조건문, 반복문 등에 사용.

예를 들어,

jinja가 포함된 쿼리문 dbt 가 jinja 를 처리한 이후 쿼리문
select *
from orders
{% if target.name == 'prod' %}
where is_deleted = false
{% endif %}
< prod 환경일 때, where 문이 존재함 >
select *
from orders
where is_deleted = false
< dev 환경일 때, where 문이 없음 >
select *
from orders
{% set cols = [
    'col1',
    'col2',
    'col3',
    'col4'
%}

select
{% for col in cols %}
cast({{ col }} as string)
{% if not loop.last %} , {% endif %}
{% endfor %}
from orders
select
cast(col1 as string),
cast(col2 as string),
cast(col3 as string),
cast(col4 as string)
from orders

 

{% set myval = 100 %} 이런식으로 set 을 사용하여 변수를 생성할 수 있음

생성한 변수는 where amount > {{ myval }} 처럼 불러와서 사용함.

 

{% if not loop.last %} , {% endif %} : 반복문 마지막이 아니라면, ' , ' 를 추가함

 

 

3. {# #} : 주석

예를 들어, {# this is a comment #} 는 컴파일할 때 무시됨

 

 

 

jinja 를 이용하여 만들어진 모델.sql 의 실제 SQL 문을 보고싶을 때

dbt compile 을 사용함

 

 

 

 

 

 

 

 

< dbt project 디렉토리 구조 gi>

 

dbt_project/
├── models/
│   ├── staging/
|        ├── stg_****.sql
|        ├── sources.yml
|        └── schema.yml  
│   ├── intermediate/
|        ├── int_****.sql
|        └── schema.yml  
│   └── marts/
|        ├── fact_****.sql
|        ├── dim_****.sql
|        └── schema.yml  

├── seeds/
├── snapshots/
├── macros/
├── tests/
├── analyses/
├── snapshots/

├── dbt_project.yml
├── packages.yml
└── profiles.yml

 

 

 

 

 

 

< models >

 

dbt 는 주로 raw 데이터staging 에 적재하고, intermediate, marts 과정을 거쳐 데이터를 처리하게 됨

이건 차원 모델링이나 메달리온 아키텍처와도 연관됨

 

models 내에 transformation 작업을 진행하는 SQL 이 들어있음

 

models 내에 SQL 파일들이 실행되면 나중에

table 이 되거나

view 가 되거나

incremental table 이 됨

그리고 이들의 관계를 기반으로 dbt 가 알아서 dependency graph 를 그린다고 함

 

 

 

 

 

models/staging 단계에서는 최소한의 transformation 작업이 진행됨

- 컬럼명 정리

- 타입 정리(캐스팅)

- null 처리

원본 데이터를 최대한 파괴하지 않는 선에서 작업 진행. lake 로 사용할 수 있도록

 

 

 

 

models/intermediate 단계에서는 복잡한 transformation 을 분리하는 작업이 진행됨

말 그대로 중간 계산 과정을 만드는 단계

- join

- aggregation

- business logic

- reusable logic 

등의 처리를 진행하게 됨

 

예를 들어

select
    customer_id,
    count(*) as total_orders,
    sum(amount) as lifetime_value
from {{ ref('stg_orders') }}
group by 1

 

데이터 처리를 위해 엄청 복잡하고 방대한 SQL 이 실행되어야 하는 경우,

이걸 한 번에 다 처리하면 디버깅하기도 어렵고 재사용하기도 어렵고 이해하기도 어려움

따라서 intermediate 과정을 중간에 넣어서, 중간 생산물들을 처리하도록 하는 것

나중에 최종 테이블 만들기 위한 transformation 작업이 복잡해지지 않고

중간결과물 재사용도 가능하도록

 

 

 

 

 

models/marts 단계에서는 최종 비즈니스 테이블을 만드는 작업이 진행됨

여기 만들어진 테이블은 BI, analytics teams 가 보고 비즈니스 결정하는 데 사용함

 

차원모델링의 최종 결과 테이블들(fact, dim tables)이 이 단계에서 생성됨

 

아래와 같이, 도메인별로 각기 다른 차원 테이블들을 만들어주면 도메인별로 분석하기가 편해짐

팀 단위의 ownership 을 구분하고 결정하기도 쉬워짐

 

models/
└── marts/
    ├── finance/
    │   ├── fct_payments.sql
    │   └── dim_customers.sql
    │
    └── marketing/
        ├── fct_campaigns.sql
        └── dim_channels.sql

 

 

그래서 models 의 전체적인 flow 를 보면 아래와 같게 됨

raw_orders
    ↓
stg_orders
    ↓
int_customer_orders
    ↓
fct_customer_revenue

 

dbt run 하면 dbt 가 위와 같은 dependency graph 를 자동으로 생성해서 처리한다고 함

(=stage 건너뛰고 marts 먼저 만드는 등 순서를 뒤죽박죽 섞어서 실행하지 않는다는 말)

 

 

 

 

 

그럼 models 마다 존재하는 schema.yml 은 뭐냐?

shecma.yml 은 

- test 를 정의하는 역할
- documentation 만드는 역할

- column 을 설명하는 역할

 

아래와 같은 schema.yml 이 있다고 해보자

version: 2

models:
  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - not_null
          - unique

 

 

models.columns.tests 항목을 통해, 해당 컬럼의 test 를 설정할 수 있음

예를 들어 null 이 존재하는지 확인하는 not_null,

중복이 없는지 확인하는 unique 등등

 

 

 

 

 

< macros >

 

재사용 가능한 SQL 함수를 정의해 두는 곳

말 그대로, 진짜 함수(파라미터 받아서 결과 반환하는)를 만들어 두는 곳

 

예를 들어, 아래와 같은 macro(generate_surrogate_key.sql)가 존재하고 내용도 아래와 같다고 하자

macros/
└── generate_surrogate_key.sql
{% macro cents_to_dollars(column_name) %}
    {{ column_name }} / 100.0
{% endmacro %}

 

함수 이름은 cents_to_dollars

함수의 파라미터는 column_name

 

아래와 같이 models 내 SQL에서 불러 사용할 수 있음

{{ cents_to_dollars('amount') }}

 

 

왜 macro 를 사용해야 하나?

바로 코드 중복을 제거해줄 수 있기 때문에

동일한 코드를 다양한 곳에서 똑같이 쓸 필요가 없고

또 macro 하나만 수정하면 나머지에 전부 적용되기 때문에 관리하기도 편함

 

 

 

 

 

 

 

< dbt_project.yml >

 

이 파일 내부에는 프로젝트의 핵심 설정 파일이 들어있음

dbt 프로젝트 전체의 동작 방식을 정의하는 중앙 설정 파일임.

(Docker Compose의 docker-compose.yml 같은 거라고 보면 된대!)

 

만약 내가 아래와 같은 sql 을 만들고 dbt run 을 하면

models/staging/stg_orders.sql
select * from raw.orders

 

dbt 는 이 sql 을 실행하기 위해 갖가지 정보가 필요함

- 테이블 어디에 만들지?

- view 로 만들어야 하나? table 로 만들어야 하나? 아니면 incremental?

- 어떤 schema 에 만들지?

- 어떤 db 에 만들지? 등등

이런 정보들을 제공하는 곳이 바로 dbt_project.yml 임

 

 

dbt_project.yml 파트별로 예를 들어봄

name: my_project
version: '1.0'
config-version: 2

 

name 은 dbt 프로젝트의 이름임

dbt init my_project 명령어를 실행해서 만들어졌을 것임

version 은 프로젝트의 버전. CI/CD 에서 사용된다고 함

config-version 은 설정파일 포맷 버전인데, 2로 고정됨. 왜 넣는지는 모르겠음

 

model-paths: ["models"]
seed-paths: ["seeds"]
snapshot-paths: ["snapshots"]
macro-paths: ["macros"]
test-paths: ["tests"]

 

위와 같은 config 를 줬기 때문에 dbt 가 어느 디렉터리에서 정보를 가져가야 할 지 알게 됨

이를테면, model-path: ["models"] 로 설정된 부분으로 인해, dbt 는 "model 들은 'models' 디렉터리 안에 있구나" 라고 인식함

 

models:
  my_project:

    staging:
      +materialized: view
      +schema: staging

    intermediate:
      +materialized: ephemeral

    marts:
      +materialized: table
      +schema: marts

 

위 config 의 'models'는 'model 에 대한 설정'을 의미함

models 이하에 나오는 config 를 통해

각 models(SQL 파일들)을 어떻게 처리해야 할 지 알 수 있음

 

my_project 는 프로젝트 이름

위의 name: my_project 와 연결됨

(프로젝트 이름을 굳이 다시 넣었다는 말은.. dbt_project.yml 내에 여러 프로젝트의 config 가 들어갈 수 있다는 말인가?)

 

materialized 가 뭐하는거야?

SQL 결과를 warehouse 에 어떤 형태로 저장할지 결정하는 부분

SQL 을 실행한 후 결과를 table 로 만들 수 있고, view 로 만들 수 있고, 혹은 incremental table 로 만들 수 있음

잘 보면, 각각의 SQL 파일들마다 materialized 가 있는게 아니고, 각 단계(staging, intermediate, marts)마다 materialized 가 존재함

즉, materialized 는 하나의 단계 전체적으로 적용된다고 볼 수 있음.

 

만약 각 테이블마다 각기 다른 materialized 를 적용하고 싶다면

모델 SQL 파일 내에 {{ config(materialized='table') }} 처럼 적용하면 됨

이렇게 적용된 설정은, dbt_project.yml 에 설정된 materialized 보다 우선시됨.

 

대부분 materialized 는 dbt_project.yml 단위로 설정함

왜냐면 각 단계가 공통의 특징을 따르기 때문에.

 

참고로 dbt 의 기본 materialized 값은 view 임

model 내에 설정 > dbt_project.yml 내에 설정 > default(view)

 

 

 

+materialized : view

여기서 view 는 실제 데이터를 저장하지 않는 가상 테이블임.

create view 명령어 실행되어 생성되는 뷰 그거 맞음

query 실행되는 런타임마다 그때 그때 계산되어 생성 및 사용되기 때문에, 항상 최신 데이터를 갖고 있음

하지만 매 런타임마다 계산되기 때문에, select 퍼포먼스가 느림
실제 테이블을 만들지 않기 때문에 차지하는 storage 적음

심지어 view 는 BI tool 에서도 접근이 가능함. BI tool 이 view 테이블에 접근할 때마다 view 쿼리를 계속 재실행한다는 말이지.....

view 는 staging 처리할 때 자주 사용된다고 하는데.. 그 이유가, 굳이 물리적인 테이블을 만들 필요가 없어서..?

staging 을 위한 sql 이 비용이 적은 이유(타입 캐스팅 혹은 컬럼명 변경 등)는 알겠어

그래서 view 처럼 런타임에 실행해도 무리가 없다는 거지

근데 staging 은 ground truth 역할(lake 마냥)을 해야하기 때문에 무조건 물리적인 테이블로 만들어둬야 하지 않나?

 

+materialized : table

여기서 table 은 실제 데이터를 저장하는 물리적인 테이블을 의미함

따라서 storage 용량을 차지하며 생성될 때 갖고있는 데이터를 그대로 보관하게 되며

하지만 view 와 달리 select 실행시 재계산을 하지 않아도 되기 때문에 select 퍼포먼스가 빠름

아래 incremental 과의 차이점은?

 

+materialized : incremental

변경분만 table 에 업데이트 함. hudi 의 upsert 와 같은 느낌인거지

즉, 기존 테이블이 없으면 새로운 table 을 만들고

기존 테이블에 데이터가 없으면 새로 데이터를 추가하고

기존 테이블에 데이터가 있으면 변경된 부분만 업데이트 함

incremental 이 적용되는 model 쿼리를 한 번 보자

select *
from orders
where updated_at > (
    select max(updated_at)
    from {{ this }}
)

위와 같이 {{ this }} 를 통해 자기 자신 테이블에서 최신 날짜만 가져온 후

최신 날짜 이후로 들어온 데이터만 추출해서 incremental 로 업데이트

 

+materialized : ephemeral

실제 table이나 view 는 생성하지 않지만

SQL 내부에 inline 된다고....(????)
ephemeral 로 설정된 model SQL 을 실행해도

warehouse 내에 object 를 생성하지 않음(그건 view 도 마찬가지잖아?-> 개발자 입장에서는 view 도 object를 만들지 않는것 처럼 보이는데, 실제로 warehouse 입장에서는 object 처럼 보인다고 함. 그래서 select * from my_view 같이 from 에 view 를 넣은 쿼리가 가능함)

다르게 말하면, ephemeral 로 만들어진 결과는 from 를 통해 불러올 수 없음(view는 이게 가능함)

ephemeral 로 설정된 model SQL 은 downstream SQL 에서 CTE 처럼 삽입된다고 함

위의 예제에서 intermediate 부분에 ephemeral 이 설정되어있고,

marts 에 table 이 설정되어 있잖아

intermediate 및 marts SQL model 이 아래와 같다고 하자

-- int_orders.sql

select
    customer_id,
    sum(amount) as total_amount
from {{ ref('stg_orders') }}
group by 1
-- fct_customer_revenue.sql

select *
from {{ ref('int_orders') }}
where total_amount > 100

이걸 실행시키면, ephemeral 로 설정된 int_orders 의 쿼리가 fct_customer_revenue 쿼리 안으로 쏙 들어감

select *
from (

    select
        customer_id,
        sum(amount) as total_amount
    from stg_orders
    group by 1

) as int_orders
where total_amount > 100

 

이걸 보고 inline 이 된다고 한 거임....

 

view 랑 ephemeral 의 차이는 알았는데, 그럼 각각은 무슨 장단점이 있을까?

- view 는 warehouse 에서 조회가능한 object를 주르륵 만들기 때문에, 여러개의 view 를 만들면 지저분해질 수 있음

ephemeral 은 지저분하지 않음(....이게 장점인가?)

- ephemeral 은 object 생성이 어려우니 디버깅하기가 어려움.

즉, view 처럼 select 해서 직접 값을 볼 수 없으니 ephemeral 로직이 잘 실행되었는지 아닌지 몰라

- ephemeral 을 사용하면 하나의 큰 nested sql 이 생성됨. 한 번에 큰 계산을 하게되는 상황이 발생할 수 있음

- downstream 에서 동일한 ephemeral 을 여러번 사용하면, 중복 계산이 되기 때문에 퍼포먼스가 좋지 않음

 

그럼 ephemeral 은 언제 쓰는거야?

warehouse object 를 만들 가치가 없는 경우에 쓴다고 함

warehouse 안에 독립된 객체(table,view 등)으로 보관할 필요가 없을 때 사용

table, view 등의 객체는 select 로 조회가 가능하고

BI tool 에서 접근도 가능하고, 디버깅 대상으로 중요함

ephemeral 은 downstream 에 필요한 작은 조치를 취하는 쿼리를 시랳ㅇ

예를 들어 phone 넘버에서 '-' 를 제거하는 쿼리나

kg을 g으로 변환하는 쿼리 등

그래서 구우우욷이 warehouse 에 객체로 남기지 않아도 됨

select 로 접근할 필요가 없고 디버깅 할 필요도 없기 때문

 

 

 

 

 

 

 

< seeds >

 

이곳에 csv 파일을 넣어두면, dbt 가 warehouse 에 자동으로 upload 한다고 함

작은 reference table 을 만드는 용도로 사용한다고 함

 

예를 들어 아래와 같은 csv 를 파일을 넣어두면....

 

country_codes.csv
currency_codes.csv

 

무슨 일이 벌어지지? 어느 db 에 만드는거야? 함부로 table 만드는 권한은 어떻게 갖고있는거지?

 

 

 

 

 

 

< tests >

 

custom test 를 저장하는 곳

예를 들어 아래와 같이 내가 원하는 테스트를 만들어 넣음 

tests/
└── assert_positive_revenue.sql
select *
from {{ ref('fct_orders') }}
where revenue < 0

 

결과가 나타난다면, 실패하는 거임...

select * 의 결과가 나타나지 말아야 함

 

ref('fct_orders') 는 fact table 대상으로 테스트를 진행한다는 말이 될 것 같음

 

그럼 여기 tests 디렉터리 내에 있는 테스트와, schema.yml 내에 있는 테스트의 차이는?

 

 

 

 

 

 

< snapshots >

 

변경 이력 추적용이라는데 뭔 소린지 모르겠음

 

 

 


< analyses >

 

ad-hoc SQL 을 저장하는 곳이라는데 뭔 소린지 모르겠음

 

 

 

 

 

< ref() >

 

위에서 계속 봐왔겠지만, ref() 는 다른 모델의 객체(table, view) 를 가져오는 역할을 함

ref() 덕분에 데이터 모델 간 의존성(dependency)가 정의될 수 있음

 

가령, 아래와 같은 모델이 있다고 하자

fct_orders.sql
select *
from {{ ref('stg_orders') }}
where amount > 0

 

여기서 {{ ref('stg_orders') }} 의 의미는, "stg_orders 모델의 결과(stg_orders.sql 의 결과)를 참조" 임

위 fct_orders.sql 이 실행될 때, {{ ref('stg_orders') }} 위치에 stg_orders 의 결과(db.table) 로 치환됨

이를테면 "select * from sales_dev.stg_orders where amount > 0"

 

구체적으로 아래 단계를 따라서 ref() 가 실행됨

- dbt 가 ref('stg_orders') 발견

- stg_orders 모델이 존재하는지 확인

- dependency 에 등록

- 실제 relation 이름을 계산 (여기서 relation 이름은, stg_orders.sql 실행 결과의 db.schema.table 이름)

- fct_orders.sql 의 from 에 위치한 ref() 를 relation 이름을 치환

 

stg_orders.sql 에 업데이트가 발생해도 위 fct_orders.sql 은 그대로 stg_orders 의 결과를 참조함

가령 stg_orders.sql 의 결과 테이블 db 가 sales_dev 에서 sales_prod 로 바뀌어도

위 fct_orders.sql 은 영향이 없음

 

 

 

그리고 ref() 를 사용하는 더 중요한 이유, 바로 dependency graph 를 생성한다는 것

dbt 는 ref() 로 연결된 모델sql들을 보고 dependency graph 를 만듦

이에 맞춰 실행할 순서를 정함

 

만약 dependency graph 가 없으면, dbt 는 어떤 모델sql 을 먼저 실행해야할지 모르게 됨

 

 

dbt run 실행시 아래와 같은 select 사용이 가능함

1. dbt run --select fct_customer_revenue

2. dbt run --select +fct_customer_revenue

3. dbt run --select fct_customer_revenue+

 

예제 DAG 하나를 기준으로 이해해보자

raw.orders
      ↓
stg_orders
      ↓
int_customer_orders
      ↓
fct_customer_revenue
      ↓
customer_dashboard

 

 

1. dbt run --select fct_customer_revenue

select 된 fct_customer_revenue 하나만 실행

즉, fct_customer_revenue 모델 sql 을 실행한다는 의미.

fct_customer_revenue 실행에 필요한 부분(upstream) stg_orders, int_customer_orders 는 이미 warehouse 존재해야 함

왜냐면 dbt 는 upstream 을 자동으로 실행하지 않기 때문

 

2. dbt run --select +fct_customer_revenue

select 된 fct_customer_revenue 를 upstream 포함하여 실행

즉, stg_orders, int_customer_orders, fct_customer_revenue 모델 sql 을 모두 실행한다는 의미

 

3. dbt run --select fct_customer_revenue+

select 된 fct_customer_revenue 과 그 이후의 downstream 을 실행

즉, fct_customer_revenue, customer_dashboard 모델 sql 을 실행한다는 의미

중간 모델을 수정했을 때, 이후 모델들까지 수정분을 적용할 때 사용되는 명령어

 

실무에서는 +fct_customer_revenue+ 도 많이 사용한다고 함.

아마 다른 모델은 건들지 않고 해당 stream 만 실행하고싶을 때 사용하는 명령어인 듯...?

 

 

 

 

 

 

헷갈릴 수 있는 부분

- dependency graph : 누가 누구에게 의존하는지 표현하는 그래프

- lineage : 데이터가 어디서 와서 어디로 흘러가는가

근데 실무에서는 결국 둘이 동일한 dag 화면을 본다고 함

 

 

 

 

 

 

 

< source.yml >

 

프로젝트 파일 내 staging 부분에 sources.yml(혹은 source.yml) 이 존재함

models/
├── staging/
│   ├── sources.yml
│   ├── stg_orders.sql
│   └── stg_customers.sql

 

이 파일은 "dbt 프로젝트의 최초 입력 데이터(input)를 정의"하는 파일

입력 데이터라고 해서 s3 나 local 의 csv, json, txt 등을 의미하는 건 아님

dbt 가 아닌 다른 프로그램(spark, kafka 등)에 의해 생성되어 warehouse 내에 테이블로 저장된 데이터를 의미함

(dbt 는 warehouse 내 테이블 데이터를 가공하는 역할이기 때문에 s3, local 에서 데이터를 가져오지 않음....)

 

sources.yml 이 아래처럼 생겼다고 하자

version: 2

sources:
  - name: raw
    database: analytics
    schema: raw
    tables:
      - name: orders
        columns:
          - name: order_id
             description: Unique order identifier
             tests:
               - not_null
               - unique
          - name: price
             description: price for order
      - name: customers
        loaded_at_field: updated_at
        freshness:
          warn_after:
            count: 1
            period: hour
          error_after:
            count: 4
            period: hour       

version: 2

sources:
  - name: raw
    description: Raw operational data
    database: analytics
    schema: raw
    tables:
      - name: orders
        description: Customer orders
        loaded_at_field: updated_at
        freshness:
          warn_after:
            count: 2
            period: hour
          error_after:
            count: 6
            period: hour
        columns:
          - name: order_id
            description: Unique order identifier
            tests:
              - not_null
              - unique
          - name: customer_id
            description: Customer identifier
            tests:
              - not_null
      - name: customers
        description: Customer master data
        columns:
          - name: customer_id
            tests:
              - not_null
              - unique

 

- sources.name : source 그룹 이름

- sources.database : warehouse 의 database

- sources.schema : warehouse 의 schema

- sources.tables.name : source 데이터가 들어있는 테이블 이름

- sources.tables.columns : source 데이터가 들어있는 테이블의 컬럼 정의
- sources.tables.columns.name : source 데이터가 들어있는 테이블의 컬럼 이름
- sources.tables.columns.tests : source 데이터가 들어있는 테이블의 컬럼 테스트
- sources.tables.loaded_at_field : source 데이터가 들어있는 테이블의 최신값 기준 컬럼
- sources.tables.freshness : source 데이터가 들어있는 테이블의 freshness 정의

 

 

 

위와 같이 정의된 source 테이블로부터 데이터를 읽는 dbt의 모델.sql 은 아래와 같음

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

 

dbt 가 위 쿼리를 실행하면 아래처럼 해석하고 실행함

select *
from analytics.raw.orders

 

dbt docs generate 를 통해 문서를 만들게 되면, source.yml 내용을 기반으로 문서에 '이런 곳에서 외부 데이터를 가져와!'라고 써 줌

 

 

테스트 실행 명령어 : dbt test 

freshness 검사 실행 명령어 : dbt source freshness

 

 

 

 

 

 

 

< source() >

 

ref() 가 모델.sql 로부터 생성된 결과 객체를 참조하는 문법이라면,

source() 는 dbt 밖에 존재하는 원본 데이터를 참조하는 문법

 

즉, 모델.sql 에서 원본 데이터를 읽을 때 source() 를 사용하고

모델.sql 에서 upstream 모델.sql 을 읽을 때 ref() 를 사용

 

예를 들어, source.yml 이 아래처럼 생겼고

version: 2

sources:
  - name: raw  <- source 이름

    database: analytics
    schema: raw

    tables:
      - name: orders  <- table 이름

 

이 orders 테이블로부터 데이터를 가져오는 모델.sql 쿼리는 아래와 같음

select *
from {{ source('raw', 'orders') }}   <- 첫번째인자 : source 이름, 두번째인자 : table 이름

 

여기서 orders 는 (dbt 와 연관없는) 외부에서 만들어진 테이블임

가령 spark, kafka 등이 warehouse 에 만든 테이블

그리고 우리는 source() 를 사용해서

dbt 에게 'raw 라는 이름의 외부 소스가 warehouse 에 존재하고, 거기에 'orders' 라는 테이블이 존재한단다'라고 알려주는 것임

 

다른 예제를 보자. source.yml 이 아래처럼 생겼고

sources:
  - name: raw

    database: analytics
    schema: raw

    tables:
      - name: orders
      - name: customers

 

이 orders, customers 테이블로부터 데이터를 가져오는 모델.sql 쿼리는 아래와 같음

select *
from {{ source('raw', 'orders') }}
select *
from {{ source('raw', 'customers') }}

 

source() 를 만나면, dbt 는 이 문장을 아래와 같이 해석하게 됨

select *
from analytics.raw.orders

 

 

 

source freshness 라는 개념이 있음

'원본 데이터가 얼마나 최신인가?'를 검사하는 설정이라고 함

a->b->c 순으로 흐르는 stream 이 있는데 a 부분에서 데이터 공급이 멈추면,

b, c 도 데이터 처리를 못하게 될 것임

"데이터를 처리하지 못한 만큼의 시간"을 계산해서 조치를 취하는 것이 freshness 설정

a 가 들어오지 않으면 dbt 가 계속 실행하는 것에 의미가 없기 때문에 dbt run 을 중단하고 알람을 발생하도록 만들꺼야

즉, freshness 를 통해 에러 혹은 알람이 발생하면, 데이터 파이프라인이 죽었다고 생각할 수 있음

 

예를 들어, 아래와 같이 freshness 를 추가한 source.yml 이 존재한다고 하자

sources:
  - name: raw

    tables:
      - name: shipments

        loaded_at_field: updated_at

        freshness:
          warn_after:
            count: 2
            period: hour

          error_after:
            count: 6
            period: hour

 

updated_at 기준으로 데이터 최신성을 판단 할 것이고,

2시간 이상 최신 데이터가 들어오지 못하면 경고,

6시간 이상 최신 데이터가 들어오지 못하면 에러를 발생시키라고 설정한 내용

 

dbt source freshness 명령어로 실행

 

dbt 가 실제로 확인하는 것은 '현재 시간'-'마지막 업데이트된 시간' 의 차이임

마지막 업데이트 된 시간은 아래처럼 구함

select max(updated_at) from raw.shupments

 

 

 

 

정리하자면, source() (source.yml) 의 의의는

- 원본 데이터 문서화 : 외부 소스가 어디인지 알 수 있음

- Lineage 생성 : DAG 의 가장 처음 부분을 설정

- Data Quality Test : 테스트도 할 수 있나봄

- Freshness Monitoring : 데이터 적재 파이프라인 장애를 감지함

- 환경 분리 : Dev, Stage, Prod 를 분리. 그럼에도 source() 를 통해 모델.sql 에는 영향이 없음

 

 

 

 

 

 

< seed >

 

dbt 의 철학은 'warehouse 내에 존재하는 데이터(테이블)를 transformation 한다'임

그래서 source() 를 통해 외부로부터 가져온 데이터를 테이블로 만들고,

그 테이블로부터 dbt 작업이 시작됨

 

source() 를 통해 유입되는 raw 데이터는 kafka, spark 등을 통해 진행되는데

근데.. 국가코드, 환율매핑, 우편번호매핑 등 정적이고 이미 정해져있는 데이터들은

굳이 spark 를 사용해서 파이프라인을 만들고 warehouse 에 넣을 가치가 없음

 

따라서 dbt 는 자체적으로, 이런 정적이고 작은 csv 데이터를 warehouse 테이블로 만들어서 사용하는데

이것이 바로 seed 라는 기능임

 

아래와 같은 프로젝트 구조가 있다고 하자

my_dbt_project
├── models
├── macros
├── seeds
│   └── country_codes.csv
└── dbt_project.yml

 

csv 파일 안에는 잘 정리된 국가 코드가 들어있음

< country_codes.csv >

country_code,country_name
KR,Korea
CA,Canada
US,United States
JP,Japan

 

 

dbt seed 명령을 실행하면, dbt 가 create table country_codes 명령어를 실행하고

country_codes  테이블에 csv 데이터를 넣게 됨

그 후, 일반적인 모델처럼 사용함

select
    o.order_id,
    c.country_name

from orders o

left join {{ ref('country_codes') }} c
    on o.country_code = c.country_code

 

 

여기서 ref 를 사용한 이유는, dbt 가 생성한 객체라서.

외부에서 생성한 객체라면 source 를 쓰겠지

 

 

seed 설정은 dbt_project.yml 에서 가능

seeds:
  my_project:
    +schema: reference
    +column_types:
      country_code: varchar(2)
      country_name: varchar(100)
seeds:
  - name: country_codes
    columns:
      - name: country_code
        tests:
          - unique
          - not_null

 

 

seed 는 데이터 크기가 매우 작을 때, 변하지 않을 때, 운영 시스템이 아닐 때, csv 를 git으로 관리 가능할 때 사용함

환율처럼 매일 변경되는 데이터나 자주 업데이트되는 데이터는 seed 대신 source를 통해 테이블로 만들어서 사용해야 함

 

 

 

 

 

 

< snapshot >

 

snapshot 은 '행(row)의 변경 이력을 저장'하는 기능

어떤 행이 업데이트 되어 컬럼 값이 덮어쓰여진 후, 해당 행에 대해 '과거 데이터는 뭐야?'라고 물어보면 알 수 없음

데이터가 덮어쓰여져서 이미 사라졌기 때문에

 

이렇게 과거 내용을 쿼리하는 케이스를 위해 snapshot 을 사용함

snapshot 을 사용하면, 테이블이 자동으로 SCD type 2 가 됨

즉, 새로운 값들이 과거 값을 덮어쓰지 않고 새로 추가됨

 

한 마디로, snapshot은 현재 상태 테이블을 이력테이블로 변환하는 용도

 

예를 들어, snapshots/customer_snapshot.sql 이라는 snapshot 을 만듦

{% snapshot customer_snapshot %}

{{
    config(
        target_schema='snapshot',
        unique_key='customer_id',  <- 기존 row 를 식별하는 키
        strategy='check',  <- snapshot 하는 방식
        check_cols=['city']
    )
}}

select *
from {{ source('raw','customers') }}

{% endsnapshot %}

 

여기서 strategy 는 check, timestamp 가 올 수 있음

timestamp : row 의 마지막 업데이트 시간(updatedat 등)이 업데이트되면, 새로운 row 로 추가(실무에서 더 선호되는 전략)

check : row 의 1개 이상의 컬럼들이 업데이트되면, 새로운 row 로 추가

- check_cols :  'check' strategy 를 사용할 때, 변경을 감지할 컬럼들의 리스트. 모든 컬럼을 감시하려면 check_cols='all' 사용

 

위와 같이 설정한 후, dbt snapshot 을 실행 (dbt run 으로 실행되지 않으나, dbt build 에는 포함되기 때문에 실행됨)

 

아래와 같이 snapshot 기능을 이용하여 SCD type2 가 된 테이블을 만들어짐

customer_id city dbt_valid_from dbt_valid_to
1 Seoul 2026-01-01 2026-02-01
1 Vancouver 2026-02-01 null

 

 

이 테이블 대상으로 과거 특정 날짜 데이터에 쿼리를 날릴 수 있음

select *
from customer_snapshot
where '2026-01-15'
between dbt_valid_from
and coalesce(dbt_valid_to,'9999-12-31')

 

 

 

append-only 인 테이블은 과거 데이터를 덮어쓰는 성격의 데이터가 아니기 때문에, snapshot 이 불필요함

snapshot 은 주로 상태(계정정보, 가격, 등급, 상태 등)가 변경되는 테이블에서 사용됨

 

 

 

 

 

 

 

 

< packages >

packages.yml 에 dbt 가 사용하는 패키지들(미리 설정된 기능들)를 리스팅해두고, 한 번에 받을 수 있음
(데이터베이스(Warehouse), Spark, Kafka 같은 외부 인프라에 직접 설치하는 프로그램 아님)
이 패키지들은 다른 dbt 개발자들이 미리 만들어둔 dbt 모델, SQL 매크로, 테스트 코드의 묶음
dbt 안에서 가져와 내 프로젝트의 개발 시간을 줄이는 용도로 사용함
예를 들어, 가장 대표적인 dbt 전용 패키지인 dbt-utils를 설치해 봄
(dbt-utils : 날짜 계산, 중복 제거 등 자주 쓰는 SQL 매크로 제공)

프로젝트 root directory 에 있는 packages.yml 파일을 아래와 같이 설정
packages:
  - package: dbt-labs/dbt_utils
    version: 1.1.1
 
아래 dbt 명령어를 통해 package 다운
dbt deps
 
명령어를 실행하면 dbt 패키지 허브에서 소스코드를 다운함
프로젝트 내 dbt_packages/dbt_utils 폴더가 생성되며,
이제부터 dbt 모델.sql 안에서 다음과 같이 매크로를 바로 사용할 수 있음
-- 두 컬럼의 값을 합쳐서 고유한 ID(해시값)를 만드는 dbt-utils 매크로 사용 예시
select
    {{ dbt_utils.generate_surrogate_key(['user_id', 'signup_date']) }} as user_hash_id,
    user_id,
    signup_date
from {{ ref('raw_users') }}



 

 

 

 

 

 

 

 

 

 

< dbt 명령어들 >

 

먼저 dbt 프로젝트 개발 흐름을 살펴보면

- 개발(모델.sql, yml 설정 등) -> compile 진행 -> run -> test -> docs 만들기 -> 배포

 

이 흐름대로 개발하며 필요한 dbt 명령어들을 알아보자

 

명령어 언제 사용되는가 의미
dbt run

특정 모델만 실행하려면
dbt run --select stg_orders

특정 계층(stage, intermediate 등)만 실행하려면
dbt run --select staging
개발(모델.sql, yml 설정 등)이 마무리 된 후 실행 dependency 순서대로 모델.sql 을 실행하고
dependency graph 를 만들고
warehouse 에 테이블을 만듦

모델.sql 을 create view, create table 명령어로 바꿔서 실행

테스트해주는 거 아님
freshness 검사해주는 거 아님
문서 만들어주는 거 아님
dbt compile SQL 디버깅 할 때 사용
실제로 생성된 SQL 문을 눈으로 확인하며 디버깅 진행
jinja 를 실제 SQL 쿼리문으로 변환
이 명령문을 통해 나온 SQL 쿼리문은 target/compiled 에 저장됨
dbt test 배포하기 전에 실행
대개 dbt run -> dbt test 순으로 실행된다고 함
데이터 품질 검사 진행

sources.yml, schema.yml, seed, test 디렉터리 내 tests 를 실행
예를 들어, tests unique 는
group by id having count(*) > 1 을 실행하여 결과가 나타나는지 확인하는 것으로 테스트를 진행한다고 함
dbt build   dbt run, dbt test, dbt seed, dbt snapshot 모두 한 번에 실행
dbt seed   seeds/mycsv.csv 등 csv 파일을 테이블로 만들 때 사용
dbt source freshness 파이프라인의 가장 처음에 실행
source freshness 실행 -> dbt build(dbt run -> dbt test)
source 의 마지막 업데이트 날짜와 현재 날짜 간 차이를 계산하여 처리(경고 혹은 에러)
dbt docs generate   문서를 생성
생성된 문서는 lineage 를 포함하여 json 파일로 저장됨

target/catalog.json: 웨어하우스(데이터베이스)에서 추출한 테이블 및 스키마 메타데이터.
target/manifest.json: 사용자가 작성한 dbt 모델, 테스트, 소스 및 DAG(계보) 정보.
target/index.html: 문서 웹사이트를 렌더링하는 기본 UI 템플릿
dbt docs serve 프로젝트 분석이 필요할 때 문서 서버를 실행함
브라우저에서 Lineage Graph, Model Doc, Column Desc 등 확인
dbt ls

특정 계층의 모델만 보고싶다면
dbt ls --select staging
  설정된 모델명들 나열
dbt debug   dbt 프로젝트의 전반적인 구성 확인

dbt 버전 확인: 현재 설치된 dbt 버전 정보
dbt_project.yml 검증: 프로젝트 구성 파일의 유효성 검사
profiles.yml 검증: 프로필 파일의 존재 여부 및 구문 검사
데이터베이스 연결 테스트: 웨어하우스 서버와의 통신 및 권한 테스트
dbt deps   packages.yml 파일에 정의된 외부 패키지(매크로, 모델, 테스트 등)를 다운로드하여 설치하는 명령어. dbt 를 위한 기능을 다운받음.

 

 

 

 

 

 

< 예상 인터뷰 질문 >

 

late arriving data 는 어떻게 처리하는가?
 

 

DAG selection syntax
dbt build --select state:modified
dbt run --select tag:daily
dbt build --select +model+

등등

 

airflow 에서 dbt 실행하는 방법
BashOperator(
    task_id="dbt_build",
    bash_command="dbt build"
)

 

'Coding Interview' 카테고리의 다른 글

[IT] Data Warehouse + Data Modeling 공부  (1) 2026.04.16
[Spark] interview questions  (0) 2026.01.22
[Data Engineer] interview questions  (0) 2026.01.16
[Airflow] interview questions  (0) 2025.12.14
[SQL] interview questions  (0) 2025.12.12

 

https://www.udemy.com/course/data-warehouse-the-ultimate-guide/

 

 

< Data Warehouse >

 

DW란, TB, PB 급의 대규모 데이터를 기반으로 분석 작업과 리포팅 작업을 하기 위해 사용되는 저장소

분석가들이 DW 에 접근하여 데이터를 분석할 수 있도록 주로 SQL 을 통해 데이터 분석작업이 진행

 

예를 들어 지난 5년간 월간 매출액 분석 등의 분석 작업을 위한 대규모 데이터가 DW 에 저장되며

의미있는 시간 내에 분석 쿼리가 동작하여 결과를 볼 수 있음

 

대규모 데이터 대상으로 쿼리하기 때문(OLAP)에, 빠른 성능이 요구되며

대규모 데이터를 저장하기 위해 확장 가능한 설계가 필요함

이를 위해 DW 는 distributed environment 를 활용하여 설계됨

 

분석가가 분석을 위해 쿼리를 실행하는 저장소이기 때문에, 사용자 친화적이어야 함

(데이터에 쉽게 접근할 수 있어야하고, 사용시 이슈가 없어야 하고, 빠른 시간 내에 결과를 받을 수 있어야 함)

 

 

다양한 도메인/팀/소스에서 생성된 데이터들을 DW 라는 곳에 모아둠

분석가들은 DW 에서 필요한 데이터들을 찾아 분석하여, 인사이트를 추출하고 비즈니스에 이용함

또한 데이터 리포팅 및 분석 결과 시각화에도 사용함

 

위와 같이 data sources 에서 DW 로 데이터를 적절하게 옮기는 작업을 ETL 이라고 함

그냥 옮기는 것이 아니고, 분석/집계 가능한 모양으로, 무결성/정합성을 지키며,

성능에 최적화된 파일포맷/구조로 옮겨야 함

 

 

 

 

< Data Lake vs Data Warehouse >

 

Data Lake 와 Data Warehouse 는 모두 centroalized data storage 로 사용되지만

저장되는 데이터 형식, 데이터 저장 목적, 데이터가 사용되는 방법 등이 다름

 

  DataLake Data Warehouse
Data Raw
가공하지 않은 데이터 그대로 저장
Transformed
SQL 을 이용한 분석/집계에 용이하도록, 데이터가 테이블 형태로 transfomed 되어있음
Tech Big data
다양한 bigdata 기술을 사용하여 구성됨
Database (SQL)
테이블로 구성된 사용자 친화적인 기술
Structure Unstructured
Json, txt, images, videos...등 다양한 형태의 raw data 가 저장될 수 있음
Structured
테이블에 들어갈 수 있는 형식적인 구조로 transformed 됨
Schema Schema 존재하지 않음
어떤 데이터든 저장 가능
테이블 스키마는 사전에 엄격하게 정의됨. 마치 RDB 처럼
Usage Not defined yet
과거 데이터를 재처리 할 때 source 로 사용될 수 있고
raw 데이터 자체에서 인사이트 추출해야 할 때 사용될 수 있음 (기계 학습에 이용하는 등)
Specific & Ready to be used
집계/분석에 사용되거나, 리포팅할 때 사용됨
Storage S3 등의 보편적인 데이터를 저장할 수 있는 저장소 SQL이 가능한 특정 DW 저장소.
Snowflake, Redshift, BigQuery 등 반드시 DW 전용 플랫폼 이용
Cost s3 등 보편적인 저장소 사용하기 때문에 저렴함  전용 DW 저장소를 사용하기 때문에 비쌈
Users Data Scientists Data Analysts, Business users

 

 

source storages 에서 수집(수입)한 데이터들을 그대로 Data Lake 에 보관

Data Lake 에 보관한 데이터를 batch 형태로 ETL 처리한 후 DW 에 넣음

 

 

 

 

 

 

< Data Warehouse Layers >

 

 

Staging : Data Sources 로부터 읽은 raw data 를 읽는 레이어

 

Cleansing : raw data 를 테이블로 만들기 위해 transformation 진행

 

Core/Data Warehouse : 데이터들을 모두 DW 테이블로 만듦

  DW 를 사용하는 사용자(분석가) 입장에서는 이 DW 레이어를 single source of truth 로 생각함

 

Data Marts : 특정 부서(영업, 마케팅, 재무 등)나 주제(지역, 시간, 비즈니스 케이스 등)에 필요한 데이터만 추출해 구성한 테이블들

  분석가들이 분석을 위해 미리 집계처리 한 데이터를 저장하는 테이블들
  효율적인 쿼리 실행을 위해 논리적/물리적 데이터 모델링(차원 모델링) 진행 (fact-dimension tables)

  대규모 DW 테이블이 아닌, 소규모 Data Mart 테이블이기 때문에, 접근하고 쿼리할 때 빠른 퍼포먼스를 볼 수 있음

  원하는 DB 를 사용하여 구축할 수 있음 (in-memory DB 등)

 

 

 

 

 

 

< Dimensional modeling >

 

대규모 데이터 분석/OLAP/집계 가 빈번히 발생하는 DW 에서는 성능이 주요 쟁점이며,

차원 모델링(Dimensional modeling)으로 테이블을 구성하면 성능적 이점을 얻을 수 있음

그래서 DW 에서 차원 모델링을 많이 차용함

 

Dimensional modeling 은 크게 두 가지 테이블을 엮어서 만듦

 

- Fact table : 비즈니스/이벤트의 측정 가능한 수치 데이터(Measures)를 담은 트랜잭션 중심의 테이블

  판매 금액, 수량 등. 비즈니스 진행하면서 행(row)이 계속 쌓이며, 크기가 큼

  PK, FK, 그리고 그 외 데이터(수치, 로그 등) 등의 컬럼들을 갖고 있음

  수치 데이터로 이루어져 있어서, 집계가 가능한 Fact table이 있고

  event, transactions, date, time 등의 로그성 데이터가 쌓이는 Fact table 이 있음

 

- Dimension table : 팩트 데이터를 분석하기 위한 기준(Who, What, Where, When)을 담은 배경 설명 테이블

  고객명, 상품명, 날짜 등. 대체로 자주 바뀌지 않으며, 크기가 작음

  fact table 의 정보를 설명하기 위한 카테고리, 그룹핑, 필터링 등으로 생각해볼 수 있음

  집계(는 가능하지만)하는 것에 의미가 없음

  PK, FK, 그 외 데이터(카테고리 등)를 가지고 있으며, PK를 이용하여 fact table 과 관계를 맺음.

 

 

 

Fact 와 Dimension table 간 '관계'가 존재함.

즉, Fact table 의 외래키(FK)를 통해 Dimension table 이 연결됨

왜 연결할까? Fact Table 의 내용을 해석하기 위해 Dimension table 을 사용하기 때문

 

만약 dimension table 을 없애고 dimension table의 내용을 하나의 fact table 에 모두 넣으면(비정규화)

dimension table 이 갖고있는 동일한 정보가 각 fact table 의 row에 따라 중복 저장되고 스토리지 비용이 증가하게 됨

만약 dimension table 정보를 업데이트 해야 한다면,

중복된 수억건의 데이터를 fact table 에서 모두 찾아 업데이트해야하는 불상사가 발생함

하지만 극단적인 분석 속도가 필요하고, join 비용(분산환경에서 데이터 운반시 드는 시간적 비용)을 줄여야한다면

하나의 fact table 로만 테이블을 구성할 수 있음.

 

dimension table 을 따로 만들고 fact table와 연결하게 되면(정규화)

다양한 fact tables 에서 하나의 dimension table 을 사용하는 경우, dimension table 을 효율적으로 사용하게 되고

모든 fact tables 가 동일한 dimension table 정보를 이용할 수 있어서 정보의 차이가 발생하지 않게 됨

또한, 쿼리를 수행 할 때 성능적 이점도 존재함.

dimension table 기준으로 먼저 필터링을 수행하여 필요한 값들만 추출한 뒤, 이를 대용량의 fact table 과 조인하여 사용함

dimension table 에서 추출한 컬럼값으로 Projection Pushdown 하여

fact table 을 쿼리하는 것이 성능적으로 훨씬 빠르고 자원 소모도 적음

 

(참고 : DW table 에는 전통적인 DB 에서 사용하는 물리적인 indexing 기능이 존재하지 않음.

전통적인 RDB 는 B-Tree 인덱스를 사용하며, 매번 데이터가 들어올 때마다 인덱스 파일을 실시간으로 업데이트 가능함

하지만 DW 는 다루는 데이터 크기가 너무 크기때문에(TB, PB 급) 매번 인덱스 파일을 업데이트 할 수 없음(업데이트 부하)

대신, Parquet 등 파일 내 메타데이터를 이용하여 Predicate(Filter) Pushdown 하거나,

Columnar file format 을 이용하여 Projection Pushdown 하거나,

FK 컬럼 기준으로 물리적인 파티셔닝 혹은 클러스터링을 진행하거나,

아주 작은 파일 헤더만 읽고 이를 이용하여 타겟이 아닌 파일은 걸러내는 블룸 필터링을 진행하여

쿼리 성능을 향상시킴)

 

 

 

 

 

< grain >

 

Fact Tables 의 단일 행(row) 이 나타내는 상세 수준을 grain 이라고 부름

grain 논리에 의해, 하나의 row는 하나의 실제 이벤트를 의미하게 됨

예를 들어, 아래 서로 다른 grain 을 갖는 두 테이블을 볼 수 있으며,
1번 transaction fact table이 더 상세한 grain 을 갖고 있음

 

1. transaction fact table :

  grain : 주문 상품 단위

  2026-04-17 / 강남점 / 김철수 / 신라면 1봉지 / 1,000원

  2026-04-17 / 강변점 / 이영희 / 버터 1개 / 1,500원

  2026-04-18 / 송파점 / 홍길동 / 콜라 1통 / 3,000원

 

2. aggregated fact table :

  grain : 상품별 하루동안 총 판매된 액수

  2026-04-17 / 강남점 / 신라면 / 총 500봉지 / 총 500,000원

  2026-04-17 / 강변점 / 버터 / 총 100개 / 총 150,000원

  2026-04-18 / 송파점 / 콜라 / 총 300통 / 총 900,000원

 

 

grain 은 가장 상세한 수준으로 설계하는 것이 좋음

왜냐하면 데이터를 합치는 것(Roll-up)은 언제든 SQL로 가능하지만,

이미 합쳐져서 들어온 데이터(Summary Grain)를 다시 쪼개는 것은 불가능하기 때문

예를 들어, 위의 2번 aggregate fact table 에서는 이미 집계가 된 상태로 row 가 존재하기 때문에

누가 얼마나 샀는지 쿼리할 수 없음

 

grain 이 무엇인지 결정되어야 fact table 에 붙일 수 있는 dimention table 들이 결정됨

 

 

 

 

< DW 에서의 Fact 와 Dimention 테이블 간 연결 >

 

PowerBI 로 그려진 2개의 fact tables, 3개의 dimension tables

 

 

위에서 두 테이블 간 PK, FK 가 존재한다고 했는데

이것은 전통적인 RDB 처럼 시스템적으로 설정되는 연결이 아님

엔지니어가 논리적으로 생각하기에 PK-FK 로 연결되는, 개념적인 것
두 테이블은 서로 join 문법을 통해 엮이면서 '연결'된다고 말할 수 있으며

join 시 사용되는 키들이 바로 PK-FK 관계에 있는 컬럼들임

 

Snowflake, Redshift, BigQuery 같은 DW 서비스에서 테이블을 만들 때 FOREIGN KEY 선언 가능하지만

선언의 의미는 단순히 "두 테이블은 이런 관계야"라고 알려주는 메타데이터(힌트)일 뿐,

실제 데이터가 들어올 때 관계를 일일이 체크하지 않음(=참조 무결성 지켜지지 않음)

 

참조 무결성을 지키지 않는 이유는 적재 성능(Ingestion Performance) 때문임

데이터가 한 줄 한 줄 들어올때마다 매번 참조무결성을 체크한다면 

대규모 데이터를 다루는 DW 테이블에서 적재 성능이 심각하게 낮아지게 됨

DW 시스템에서 참조 무결성을 지원하지 않기 때문에, 

데이터 엔지니어는 ETL 파이프라인에서 데이터를 무결성 이슈 없이 처리해야 함

 

FOREIGN KEY 선언함으로써, DW 엔진에게 두 테이블의 관계에 대해 알려줄 수 있게 되고,

DW 는 FK 메타데이터를 참고해서 더 빠르고 최적화 된 쿼리 실행계획을 만들 수 있음

즉, PK-FK 선언이 의미없이 형식적으로 하는 것이 아니라 실제로 성능에 영향을 줄 수 있는 부분이 됨

(PK-FK 선언은 분석가가 테이블 간 관계를 더 이해하기 쉽게 만든다는 부가적인 효과도 줌)

 

참고로 delete/update cascade 는 지원하지 않음

DW 의 데이터는 '한 번 쓰고 여러번 읽는다', 'fact 테이블은 업데이트 하지 않는다' 는 원칙이 있다고 함

만약 업데이트/삭제가 발생한다면, 데이터의 역사성/신뢰가 깨질 수 있음(히스토리로 사용 불가)

 

 

 

< Schema >

 

https://dev.to/puneet_verma_aa1b0f7536c6/understanding-star-schema-vs-snowflake-schema-4hlj

 

Fact, Dimension 테이블들을 어떻게 구성하느냐에 따라서도 성능이 달라짐

자주 사용되는 구성은 Star Schema 와 Snowflake Schema.

이 둘은 정규화(Normalization) 정도에 따라 구분됨

 

 

  정규화 비정규화
장점 - 데이터 중복이 많이 사라지며 그에 따라 storage 비용 절약
- 특정 속성 변경시, 작은 차원테이블 하나만 수정하면 되기 때문에 유지보수 용이
- 정규화 진행하면 데이터는 한 곳에 존재하기 때문에 수정될 때 데이터 간 불일치 줄임(데이터 무결성)
- join 횟수가 적기 때문에 검색/집계 쿼리 성능이 좋음
- DW 엔진은 단일 테이블 대량 스캔에 더 최적화 된 경우가 많음
- 적은 테이블만 사용하면 되기 때문에, 사용자 친화적임
단점 - join 진행시 네트워크 셔플링 이슈로 성능 악화
- 여러 테이블 결합해야해서 쿼리가 복잡해짐
- 데이터 중복 발생 가능하며 그에 따라 storage 비용 증가
- 데이터 하나 수정하려고 해도, 모든 중복데이터 찾아 수정해야해서 업데이트하기 어려움
- 비정규화 진행될수록 데이터가 중복되기 때문에 무결성 깨질 확률이 높음

 

 

 

 

DW, Data Mart 에서 Star Schema 가 보편적으로 사용됨

하지만 비즈니스에 따라, 그리고 무엇을 우선순위로 둘 것인지(성능? 정확성?)에 따라 사용되는 스키마가 다름

 

 

 

 

 

 

< Fact Tables 상세 : Additive >

 

Fact table 이 갖는 '수치' 데이터는, '숫자를 더했을 때 비즈니스적 의미가 있는지'에 따라 각각 다음으로 나눌 수 있음

 

Additive facts

- Additive facts : 어떤 차원(시간, 지역, 상품 등)으로 더해도 항상 의미가 있는 수치

  시간, 매출액, 판매 수량, 비용

  오른쪽 위 fact table 의 units 은 카테고리별로 합산해도 의미가 있고, name으로 합산해도 의미가 있음

 

  '시간' 차원으로 매출 합산 : 1월 매출 + 2월 매출 = 1~2월 총 매출 (의미있음)
  '지역' 차원으로 매출 합산 : 서울 매출 + 부산 매출 = 전국 총 매출 (의미있음)

  '상품' 차원으로 매출 합산 : 라면 매출 + 콜라 매출 = 전체 상품 매출 (의미있음)

 

 

 - Semi-additive facts : 특정 차원(주로 시간)으로는 더하면 안 되지만, 다른 차원으로는 더해도 되는 수치

  은행 잔고, 창고 재고량, 현재 인원 등

  오른쪽 위 fact table 의 balance 는 portfolio 단위로 합산하는 것이 의미가 없음 (아래 붉은 엑스)

  balance 는 그대로인데 날짜마다 합산하면 중복된 값을 더하는 것이기 때문

  대신 portfolio 별로 날짜에 따라 balance 를 합산하는 것은 의미가 있음 (added across types)

  1월 1일에 모든 portfolio(USD cash, stocks) 가 갖는 금액이 170달러

  1월 2일에 모든 portfolio(USD cash, stocks) 가 갖는 금액이 270달러
  1월 3일에 모든 portfolio(USD cash, stocks) 가 갖는 금액이 160달러.

  이렇게 날짜별로 내 금액이 얼마나 바뀌고있는지 확인하는 차원에서 의미가 있음

 

  '지역' 차원으로 재고 합산 : A창고 재고(10개) + B창고 재고(20개) = 전체 재고 30개 (의미있음)
  '시간' 차원으로 재고 합산 : 월요일 재고(10개) + 화요일 재고(10개) = 총 재고 20개? (의미없음)

  월요일에 있던 재고가 화요일에 그대로 있는 경우가 있으니 총 재고 20개는 의미가 없게 됨

  이렇게 시간 차원으로 합산이 불가능한 경우에는 '평균'이나 '마지막 값'을 사용해야 함

 

 

 

- Non-additive facts : 어떤 차원으로도 절대 더해서는 안 되는 수치

  비율(ratio), 단가(price), 할인율(%), 온도, 단가, 이자율 등

  더하는 행위가 비즈니스적으로 전혀 의미 없음

  오른쪽 위 fact table 의 price 는 더하는 것에 의미가 없고 (의미가 있는 '날짜별 매출'은 units 과 곱해야 함)

  category 별 판매 금액을 더하는 것도 의미 없음(아래 붉은 엑스)

 

  '상품' 차원으로 할인율 합산 : A상품 할인율(10%) + B상품 할인율(10%) = 총 할인율 20%? (의미없음)
  '시간' 차원으로 온도 합산 : 어제 온도(20°C) + 오늘 온도(20°C) = 이틀간 총 온도 40°C? (의미없음)

  할인율처럼 Non-addictive 수치값을 비즈니스에 사용하려면

  할인율을 미리 계산해두지 말고 할인액, 매출액을 따로 컬럼으로 저장해두고 할인율을 구해서 사용하도록 해야 함

  

 

 

 

 

< Fact Tables 상세 : Null >

 

대부분의 DW 엔진에서 집계 처리할 때 null 값을 자동으로 처리해줌

혹은 엔지니어가 직접 COALESCE 이나 NULLIF 등의 함수를 사용하여 처리할 수 있음

fact table 은 null 이 필요한 경우도 존재하기 때문에, 남겨둘지 대체할지 고려해야 함

 

문제가 되는 부분은, FK 가 null 인 경우임

 

FK 에 null 값이 존재하는 경우

 

DW 는 참조무결성을 지원하지 않기 때문에 엔지니어가 직접 무결성을 지키도록 쿼리/프로그래밍 해야함

따라서 FK 가 null 인 경우가 발생하지 않도록 조심해야 함

 

FK 에 null 값이 존재할 때, null 을 -1, 999 등 사용하지 않을 값으로 치환할 수 있음

 

null 값을 999 로 치환

 

 

 

 

 

 

 

 

< Fact Tables 상세 : Year-to-Date >

 

DW 를 사용하는 분석가들은 데이터를 통해 연간 누계 또는 월간 누계 등

특정 날짜 동안의 누계를 확인하고 싶은 경우가 많음

엔지니어들은 '미리 계산한 누계값을 컬럼에 넣어두자'는 유혹을 받을 때가 많음

그러지 말아야 함

만약 누계에 사용된 날짜값의 row 가 업데이트되면, 해당 누계값도 바뀌어야 하는데

영향을 받는 누계값이 많다면, 업데이트로 인한 비용이 상당히 많아지게 될 것

 

그럼 어떻게 해야하느냐?

미리 계산한 누계를 컬럼으로 만들거나 하진 말고,

fact table 의 grain 에 따라, 원천 값만 저장하고

누계는 나중에 필요 할 때마다 계산해서 사용 (BI 툴 등을 이용하여 계산)

 

 

 

 

 

 

 

 

 

< Fact Tables 상세 : 종류 >

 

Fact table 은 세 가지 종류가 있음

'Transactional', 'Periodic Snapshot', 'Accumulating Snapshot' 

 

- Transactional Fact Table :

 

하나의 row 는 하나의 이벤트, 하나의 트랜잭션을 의미함

가장 낮은 단위의 grain 을 갖음

가장 흔한 fact table 이며, 매우 유연하다고 함

많은 dimension tables 와 연결되어 있는 경우가 흔함(FK 를 많이 갖고 있음)

트랜잭션이 발생할때마다 row 가 하나씩 늘어나는 구조이기 때문에 크기가 크고 양이 늘어나는 속도도 빠름

  

 

- Periodic Snapshot Fact Table :

 

하나의 row 는, 특정 기간 동안 쌓인 이벤트 측정값들을 한번에 집계한 값을 의미함

집계된 값이 row 로 추가되는 구조라 크기가 엄청 크지 않고 늘어나는 속도도 변변찮음

적은 dimension tables 와 연결되어 있는 경우가 흔함(FK 를 조금만 갖고 있음)

특정 기간 동안 이벤트가 전혀 발생하지 않았다면 null 혹은 0 으로 표현하게 됨

 

 

- Accumulating Snapshot Fact Table :

 

 

Periodic Snapshot Fact Table 과 비슷하지만, 특정 기간동안 누적 집계한 값이 아닌

'특정 행동이 시작하고 끝나는 동안의 이벤트를 누적 집계'한 값을 갖음

예를 들어 하나의 프로젝트, 하나의 프로세스, 하나의 작업 과정 등

택배 운송장 번호로 볼 수 있는 배송 현황(출고부터 도착까지 단계 단계 정보들)이라고 생각하면 이해하기 쉬움

Accumulating Snapshot Fact Table 를 통해 워크플로 분석이나 프로세스 분석, 프로젝트 단계 등을 분석하기 좋음

셋 중 가장 덜 사용되는 fact table 이라고 함

Date/Time 값을 FK 로 사용하는 경우가 많음

 

 

세 가지 fact 테이블 종류 비교

 

 

 

 

 

 

< Fact Tables 상세 : Factless Fact Table >

 

 

측량된 수치나 숫자값 등이 아닌,

특정 이벤트 / 특정 거래의 차원적 측면만 기록하는 fact table 이 존재함

즉, 테이블 내에 fact 가 존재하지 않고, (위의 예제와 같이) dimentional 값만 존재함

 

 

 

 

 

 

< Fact Tables 상세 : Kimball 차원 모델링으로 fact table 디자인하기 >

 

fact table 을 디자인하기 위한 4가지 지침이 있으며 Kimball 차원 모델링 이라고 부름

Kimball 차원 모델링의 4가지 지침을 잘 따라서 디자인하면 좋은 fact table 이 완성됨

 

- 비즈니스 프로세스 선택 (Select the Business Process) :

  분석할 핵심 비즈니스 절차(예: 주문, 매출, 클릭)를 정의

  매출을 분석하고싶은가? 주문 프로세스를 분석하고싶은가?

  DAU/MAU를 분석하고 싶은가? 비즈니스 절차를 분석하고 싶은가? 등등

  마케팅, 물류, 결제 등을 한 번에 모델링하려고 하지 말고, 하나의 프로세스에 집중해야 함

 

- grain 설정 (Declare the Grain) :

  fact table의 단일 행이 나타내는 grain을 가장 상세한 수준으로 결정

  예를 들어, '하나의 주문건', '하룻동안의 판매 정보', '개별 상품 품목' 등

  가장 상세한 수준으로 결정해야 나중에 세세한 비즈니스 질문에 답할 수 있음

 

- 차원 식별 (Identify Dimensions) :

  '누가, 무엇을, 어디서, 왜, 어떻게'에 해당하는 문맥 데이터(예: 고객, 제품, 시간, 매장)를 정의

  이 정보를 기반으로 Fact table 과 연결(join) 될 Dimension Tables 가 결정

  즉, Fact Table 에서 dimension table 과 연결되는 컬럼이 됨
  예를 들어,

  누가: 고객(Customer)
  언제: 날짜/시간(Date/Time)
  어디서: 배송지/상점(Location)
  무엇을: 상품(Product)

 

- fact 식별 (Identify Facts) :

  비즈니스 프로세스에서 측정 가능한 수치 데이터(예: 판매 금액, 수량)를 정의

  추후 Fact Table 의 컬럼이 됨

  예를 들어, 판매 수량, 개당 판매 금액, 할인 금액, 세금, 순매출액 등

 

 

예를 들어봄.

나는 캐나다의 대형 커피 프랜차이즈를 운영하고 있으며,

"어떤 매장에서, 어떤 시간대에, 어떤 메뉴가 가장 많이 팔리는지, 

그리고 쿠폰 사용이 매출에 어떤 영향을 주는지"를 알고 싶음

 

- 비즈니스 프로세스 선택 : "고객의 결제(Point-of-Sale Sales)" 프로세스를 선택
  왜냐면 이것이 매출 분석의 가장 기본이 되는 핵심 활동이기 때문

 

- grain 설정 : 영수증에 찍힌 개별 항목(Line Item) 하나를 grain 으로 설정

  고객이 어떤 상품을 구매하였는지 row 단위로 알 수 있도록.

  만약 '영수증 한 장'을 단위로 잡으면, 고객이 '커피 3잔', '라떼 2잔' 샀을 때 각각의 판매량을 분석할 수 없게 됨.

  개별 아이템 단위로 잡아야 가장 세밀한 분석이 가능

 

- 차원 식별 : grain(영수증 항목)을 설명하기 위해 필요한 모든 관점을 나열 (누가, 언제, 어디서, 무엇을)
  날짜(Date): 판매가 일어난 날짜 (연, 월, 일, 요일, 공휴일 여부 등)
  시간(Time): 판매 시간대 (시, 분, 아침/점심/저녁 피크타임 구분 등)
  매장(Store): 판매가 일어난 장소 (밴쿠버 지점, 토론토 지점, 매장 크기 등)
  상품(Product): 팔린 물건 (아메리카노, 라떼, 사이즈, 카테고리 등)
  결제 수단(Payment): 신용카드, 현금, 모바일 페이 등
  프로모션(Promotion): 적용된 쿠폰이나 할인 이벤트 이름

 

- fact 식별 : 영수증 항목 하나당 기록될 '숫자(측정값)'를 정함
  판매 수량 (Quantity): 몇 잔 팔렸나? (Additive)
  총 판매 금액 (Gross Revenue): 할인 전 가격 (Additive)
  할인 금액 (Discount Amount): 쿠폰 등으로 깎아준 금액 (Additive)
  순 판매 금액 (Net Revenue): 실제 결제된 금액 (Additive)
  단가 (Unit Price): 상품 하나당 가격 (Non-additive) - 합산하지 않고 참조용으로만 사용

 

최종 Fact Table 은 다음과 같이 생성됨

 

 

 

 

 

 

 

 

 


< Fact Tables 상세 : PK 로 사용할 컬럼 정하기 >

 

Fact Table, Dimension Table 모두 natural key 보다 surrogate key 를 사용하는 것이 좋음

(단, natural key 중 '날짜값'은 PK 로 사용 가능함)

 

- natural key 는 길고 복잡한 경우가 많기 때문에 상대적으로 간단한 surrogate key를 사용하는 것이 좋음

  간단한 key 로 PK-FK 를 설정하게 되면 join 성능에 긍정적인 영향을 줌

 

- natural key 는 업데이트 되는 경우가 있기 때문에 변하지 않을 surrogate key 사용

 

- 여러 source 로부터 데이터를 가져왔는데 하필 natural key 들이 서로 겹치는 상황이 발생하는 경우

  surrogate key를 사용하여 데이터를 구분할 수 있음

 

- natural key 가 '알 수 없음', null 등의 값일 때 surrogate key 를 사용하면 -1, 9999 등으로 대체해서 사용 가능

 

 

그럼 natural key 는 버려야하는가? 하는 질문을 할 수 있음

natural key 는 버려도 되지만, 비즈니스적 의미는 갖고 있기 때문에

look up 테이블 하나를 만들어서 surrogate key 와 natural key 를 연결시켜 natural key 도 남기는 방법을 추천함

분석가 등의 사용자 입장에선 surrogate key 보다 natural key 로 쿼리하는 것이 더 자연스러움

 

 surrogate key 가 존재해도 natural key 의 '유일성'이 사라지는 것은 아니기 때문에

 surrogate key (PK) 가 다르지만 동일하게 처리해야 하는 rows 를 natural key 로 묶어줄 수 있음

예를 들어 kim 이 이사가서 주소가 바뀐 경우(SCD type2로 처리)

 

Row 1: SK: 101, NK: kim@email.com, City: Vancouver, Current: N

Row 2: SK: 505, NK: kim@email.com, City: Toronto, Current: Y

 

이 때 Surrogate Key 는 서로 다르지만, Natural Key 는 동일하기 때문에

이 둘이 같은 사람이구나! 라는 것을 알 수 있음

 

또한, source DB 를 뒤져봐야 하는 경우 Natural Key 를 사용할 수 있음

왜냐면 surrogate key 는 우리가 만든 값이고 sourceDB 에는 없으니까

 

 

 

 

 

 

 


< Conformed Dimension Tables >

 

 

서로 다른 두 fact tables 이 공유하는 dimension table을 Conformed Dimension Tables 라고 하며,

서로 다른 두 fact tables 을 공통된 차원(예: 날짜, 매장 등)을 기준으로 연결하여 분석하는 행위를 Drill across 라고 함

이렇게 dimension 을 연결함으로써 두 가지 fact tables (위 예제의 sales 와 cost)를 같이 연결하여 분석 가능함

 

 

위와 같이, 날짜(Time/Date) dimension 을 연결함으로써,

각 월의 비용cost 과 판매sales 를 같이 비교, 분석 할 수 있음

 

 

 

위 스샷은 서로 다른 두 개의 fact tables 가 하나의 conformed dimension table 을 사용할 때

서로 다른 FK 를 사용하도록 설정된 것임

이게 가능한 이유는, dimension table 에 Date_FK 와 DateMonth_FK 가 join 할 수 있는 컬럼들이 모두 있기 때문

예를 들어,

 

 

- Sales Fact Table 과 조인할 때 : Fact_Sales.Date_FK (20220101) = Date_SK
- Cost Fact Table 과 조인할 때: Fact_Cost.DateMonth_FK (2022-01) = Month_SK

 

아래는 Drill Across 수행하는 SQL 예시

-- 1. 일 단위 매출을 월 단위로 집계
WITH monthly_sales AS (
    SELECT d.Month_SK, SUM(s.amount) as total_sales
    FROM Fact_Sales s
    JOIN Dim_Date d ON s.Date_FK = d.Date_SK  -- 일 단위 조인
    GROUP BY d.Month_SK
),
-- 2. 월 단위 비용 집계
monthly_cost AS (
    SELECT DateMonth_FK as Month_SK, SUM(cost) as total_cost
    FROM Fact_Cost  -- 이미 월 단위이므로 바로 집계
    GROUP BY DateMonth_FK
)
-- 3. 공통 차원(Month_SK)을 기준으로 Drill Across
SELECT s.Month_SK, s.total_sales, c.total_cost
FROM monthly_sales s
JOIN monthly_cost c ON s.Month_SK = c.Month_SK;

 

 

비록 팩트 테이블에 저장된 FK의 형태나 상세도는 다르지만, 

이들이 바라보는 Dim_Date라는 마스터 테이블은 하나임

두 팩트 테이블의 FK 값이 20220101과 2022-01로 서로 다르더라도

이 값들이 동일한 차원 테이블의 유효한 키(혹은 속성)라면 Drill across 분석에 아무런 문제가 없음

FK의 모양이 같은 게 중요한 것이 아니라, 차원 테이블 내의 속성 정의(이름, 범위, 값의 의미)가 전사적으로 동일해야 함

 

 

 

 

< 그 외 Dimension tables 종류 >

 

- Degenerate Dimensions : 속성 정보가 거의 없고, 오직 식별 번호만 의미가 있는 경우

  Dimension table 로 따로 설정하지 않고, 바로 fact table 에 직접 저장

  예를 들어 영수증 번호, 송장 번호 등

  영수증 번호는 그 자체로 의미가 있을 뿐 추가로 설명할 속성이 없음

  이를 위해 Dimension table 을 만들면 join 만 늘어남

  (이런 개념이 왜 필요한거지? 왜 설명한거야 이건..? 그냥 fact table 의 일반적인 컬럼 중 하나 아니야?

  -> 'dimension' 이라는 것은, 데이터를 식별하고 묶어주는 핵심적인 역할인데

  영수증 번호, 송장 번호 등의 값은 'dimension' 역할이 가능하기 때문에 차원이라고 부르는 것임

  표면상으로 보면 그냥 fact table 의 컬럼 중 하나일 뿐임)

 

- Junk Dimensions : fact table 에 있는 여러 개의 자잘한 flag, indicator 등을 모아둔 dimension table

  Y/N, 1/0 등으로 나타나는 컬럼들.

  예를 들어, 결제 여부(Y/N), 성별(0,1), 취소 여부(Y/N), 배송 여부(Y/N) 등

  fact table 에서 발생 가능한 모든 조합(Y/Y/N, N/N/N, N/Y/Y 등)을 미리 계산해두고 dimension table 로 만들고 연결 

  왜 이렇게하느냐? 

  플래그들이 너무 많아서 Fact 테이블이 너무 넓어지는 것을 방지하기 위함

 

- Role-playing Dimensions : 하나의 Fact 테이블에서 여러 가지 역할로 재사용되는 Dimensions table.

  예를 들어 fact table 에 주문일, 배송일, 결제일 세 가지 날짜 컬럼이 존재

  그리고 우리는 Dim_Date 라는 dimension table 하나만 만들고, 주문일/배송일/결제일 분석에 사용.

  JOIN Dim_Date AS Order_Date ON ...
  JOIN Dim_Date AS Ship_Date ON ...

  동일한 구조와 데이터를 가진 dimension table을 논리적으로 분리하여 사용.

  이는 데이터 일관성을 유지하기 위함

 

 

 

 

 

 

 

 

 

 

 


< SCD(Slowly Changing Dimension) >

 

Dimension Table 데이터가, 시간이 지남에 따라 어떻게 변화를 관리할 것인가를 다루는 개념
Dimension Table 의 속성이 변경될 때, 그 변경 사항을 어떤 방식으로 반영할지를 정의하는 전략

어떤 전략을 사용할지 결정하는 것은 '비즈니스 사용자'들임.

분석가들이 '이력도 필요해요'라고 하면 type2를 사용하는 것이고 

'절대 바뀌면 안 되는 값이에요' 하면 type0 을 사용하는 것이지

고객 정보, 제품 정보, 주소, 부서 같은 차원 데이터는 (자주는 아니고) 가끔씩 변하기 때문에 "Slowly Changing" 이라고 부름

주요 유형 (Type 0 ~ Type 3)

1. SCD Type 0 – 변경 없음
- 변경이 발생해도 원래 값을 그대로 둠.

- 데이터의 정체성을 유지하기 위함.
- 예시: 생년월일, 최초 가입일, 원래 주문번호, 이름(고객 이름을 바꾸더라도, 데이터 웨어하우스에는 최초 등록된 이름만 저장)

2. SCD Type 1 – 덮어쓰기 (Overwrite)
- 변경 발생 시 기존 값을 그냥 덮어씀. 과거 데이터는 사라지고, 항상 최신 상태만 유지.
과거는 중요하지 않고 최신 상태만 필요한 경우에 사용
- 예시 : 전화번호(과거 전화번호는 의미가 없음), 이메일 주소

  고객 주소가 "서울" → "부산"으로 변경되면 차원 테이블에는 부산만 남음.
  "과거에 고객이 서울에 살았다"는 정보는 알 수 없음.

- 과거 데이터를 포함하여 집계하는 경우, (업데이트가 진행된)현재 속성이 적용되어 데이터 왜곡이 발생할 수 있음

  즉, 과거 데이터 값들이 현재 정보 기준으로 집계되는 문제가 발생할 수 있음

 


3. SCD Type 2 – 이력 관리 (History Tracking). Default Strategy
- 변경 발생 시 데이터의 과거 이력을 보존하기 위해 새로운 row를 추가해서 이력 관리.
- 과거 이력 분석이 필요한 경우 사용
- 보통 효력 시작일 (start_date) / 효력 종료일 (end_date) / is_current 같은 컬럼을 둠.


- 예시 : 고객 주소가 "서울" -> "부산" 으로 변경되었다면 차원 테이블에는 두 가지 row 가 다 들어있게 됨
  고객  주소  create_time  update_time  is_current
  eye  부산   1991-06-25   2025-11-04  Y
  eye  서울   1991-06-25   1991-06-25  N

 

- 예시 : 이커머스에서 카테고리가 재분류(Electronics -> Fashion)되었다면

  Product_SK  Prod_ID (NK)  Category     Start_Date   End_Date     Is_Current
  77                 W-100             Electronics  2025-01-01  2026-04-15   N
  89                 W-100             Fashion       2026-04-16  9999-12-31   Y

 

dimension table 이 업데이트(동일한 제품의 카테고리만 업데이트) 된 이후

fact table 는 Product_SK 가 77대신 89를 사용하여 row 를 만들게 될 것임

"W-100의 총 매출" 등의 쿼리를 하려고 where sk=89 처럼 쿼리하면 77을 갖고있던 row를 다 무시하게 될 것임

이런 경우, natural key를 사용하여 dimension table PK 는 다르지만 서로 동일한 제품이구나!를 파악함

예를 들어, 

SELECT D.prod_id, SUM(F.amount)
FROM fact_table F
JOIN dim_table D

ON F.product_fk = D.product_sk
WHERE D.Prodid = 'W-100'

GROUP BY D.prod_id

 


- 예시 : 고객 주소가 Vancouver 에서 Toronto 로 변경되었다면,

  과거가 되어버린 데이터의 end_date 를 "변경된 날의 어제 날짜(yesterday)" 로 설정하고

  새롭게 추가된 데이터의 start_date를 "변경된 날" 로 설정. end_date는 먼 미래로 설정

  Customer_SK  Email (NK)         City            Start_Date    End_Date    Is_Current

  101                   kim@mail.com  Vancouver  2025-11-01  2026-02-02  N

  505                   kim@mail.com  Toronto       2026-02-03  9999-12-31  Y

 

만약 "kim 의 총 매출을 보여주고, 현재 어느 도시(Toronto)에 사는지도 같이 표시해줘" 라고 쿼리해야 한다면

is_current 가 y 인 것을 추가로 필터링해줘야 함

예를 들어,

SELECT 
    d_curr.email,
    d_curr.city AS current_city, -- 현재 살고 있는 도시(Toronto)
    SUM(f.sales_amount) AS total_sales
FROM fact_sales f
JOIN dim_customer d_all ON f.customer_sk = d_all.customer_sk -- 과거/현재 모든 SK와 조인하고 그리고
JOIN dim_customer d_curr ON d_all.email = d_curr.email -- 같은 이메일 중
  AND d_curr.is_current = 'Y' -- 현재 유효한 행의 정보만 가져옴.
GROUP BY d_curr.email, d_curr.city;  -- is_current='Y' 에 매칭되는 email, city 단 하나씩만 나옴.

 

이 예제에서 총 세 개의 테이블이 합쳐짐

1. fact_sales (fact_table) : d_all 과 조인되어, dim_customer 정보를 모두 fact_table 로 가져옴

2. d_all : 아무런 필터나 추가 조치없이 그냥 fact_table 에 그대로 존재하는 dimension table

3. d_curr : is_current='Y' 인 email 과 city 만 갖고옴. d_all 와 join 후, d_all 의 모든 row 에 매칭됨

 

End_Date 를 '9999-12-31' 처럼 현재 유효한 데이터의 종료일을 크게 잡은 이유는,

쿼리에서 WHERE '2026-04-24' BETWEEN Start_Date AND End_Date 처럼

특정 시점의 유효 데이터를 찾기 매우 편해지기 때문

 

최신 정보만 필요할 때, WHERE Is_Current = 'Y' 필터를 걸어 성능 최적화 가능

 

 


4. SCD Type 3 – 제한된 이력 관리
- 변경 전/후 값을 컬럼으로 저장. 행을 새로 만들지 않고, 테이블에 '이전 값(Previous Value)' 컬럼을 추가하여 이력 관리

- 두 번 이상 업데이트하면 과거 데이터부터 소실됨
- 예시
  고객  현주소  전주소
  eye  부산      서울

 

 

 

 

 

< 논리적 성능 최적화 >

 

kimball 모델링을 따라 fact, dim tables 를 star schema 로 만든다고 하자

 

- dim table 에 Surrogate Key 를 사용

  이유는 위의 "< Fact Tables 상세 : PK 로 사용할 컬럼 정하기 >" 부분을 참고

- 비즈니스별로 SCD type 을 고려

  history tracking 이 필요한 비즈니스에서는 type2가 필요하지만 

  굳이 필요없다면 공간 낭비, search query 성능 낭비해가면서 history 를 유지할 필요 없는 type1 사용

- snowfalke schema 보다는 star schema (비정규화 진행)

  dim table 내 데이터 중복 관리와 사이즈가 커지는 문제보다, join 으로 인한 성능 하락 이슈가 더 크리티컬하기 때문

 

 

 

< 물리적 성능 최적화 >

 

kimball 모델링을 따라 fact, dim tables 를 star schema 로 만든다고 하자

 

- Partitioning, Bucketing

  fact tables 를 날짜 단위로 파티셔닝함(물론 '날짜' 뿐만 아니라 cardinality 가 낮은 '지역' 등으로 해도 됨..)

  partition pruning 을 통해 IO 를 획기적으로 줄일 수 있음

  하지만 cardinality 가 높은 key 로 파티셔닝하게되면 small file problem 을 발생시킬 수 있으니 조심

  bucketing 을 통해 shuffle 을 피하여 join 성능을 올릴 수 있지만,

  bucketing 이 진행된 key 기준으로만 join 성능이 좋다는 단점이 있고,

  또 '한 번 정한 버켓의 개수'를 늘릴 수 없다는 단점이 있음

  따라서 데이터가 추가, 업데이트 되는 것을 반영하지 못하거나, 재처리해야함

  (https://eyeballs.tistory.com/206  의 < join 및 shuffle 이 진행되는 구체적인 과정 > 부분 참고)

- Columnar Storage 사용

  이건 parquet, orc 를 말하는 것임

  filter pushdown 을 통해 IO 를 줄일 수 있음

- Periodic Snapshot Fact Table 사용

  거대한 fact table 대상으로 자주 집계를 진행하는 부하를 줄이고자,

  자주 처리되는 집계값을 미리 계산해 둔 table 을 만들어서 사용

  엄밀히 말하면, Periodic Snapshot Fact Table 는 쿼리 최적화를 위한 테이블은 아니고

  필요한 값을 미리 계산해둬서, 사용자가 계산 할 필요가 없게 만들어주는 테이블임

  시점별 상태를 기록하는 용도로 사용됨

- Materialized view 를 사용

  Periodic snapshot fact table 과 비슷하면서도 조금 다른 기능

  Materialized view 는 DB엔진 자체에서 추가해주는 기능임

  사용자는 쿼리 실행시 Materialized view 의 존재를 모르고 쿼리하고,

  DB에서 Materialized view 를 사용하여 쿼리 처리함

  Materialized view 는 DB 엔진에 의해 자동으로 채워짐(incremental 혹은 full 방식으로)

-  join 은 broadcast join 으로

  https://eyeballs.tistory.com/206  의 < join 및 shuffle 이 진행되는 구체적인 과정 > 부분 참고

- Iceberg, Hudi 의 bloom filter indexing 사용

  검색시, "내가 찾는 데이터가 확실히 없는 파일"을 건너뛸 수 있어서 full scan 방지 가능

- parquet 데이터 저장 할 때는 정렬한 후에 저장

  parquet 포맷을 사용하여 데이터를 저장하기 전에 정렬을 미리 해두고 저장해야 좋음

  그래야 parquet 파일별로 각기 의미있는 통계 메타데이터가 갖춰지고, 이를 이용해 Predicate pushdown 가능

  예를 들어, product_key 1번부터 1000번까지의 데이터가 (정렬하지 않은 상태로) 모든 파일에 골고루 퍼져 있다면

  어떤 파일을 열어도 메타데이터 통계 값에 Min: 1, Max: 1000이 기록되니, 의미가 없게됨

  만약 정렬된 상태로 저장했다면, 첫번째 파일에 1~100번, 두번째 파일에 101~200번... 등의 통계값이 저장되고

  predicate pushdown 에 적합하게 됨

  (parquet 추가 설명 : https://eyeballs.tistory.com/241)

 

Dimension table (FK) 를 사용해서 fact table 을 쿼리하는 것은 어떤 의미에서 성능향상이 되는걸까?

join 시 사용되는 FK 의 특정값을 이용하여 (predicate pushdown 을 통해) IO 비용을 줄일 수 있음

partitining 을 진행할때는 cardinality 가 낮은(지역, 날짜 등) FK 대상으로만 진행해야 함

(그러지 않으면 small file problem 발생..)

정리하면, FK만 쓴다고 빨라지는 것이 아니라

FK를 기준으로 데이터를 물리적으로 정렬하여 저장했기 때문에 엔진이 메타데이터를 보고

'안 읽어도 되는 파일'을 걸러낼 수 있어서 빠른 것 (predicate pushdown)

 

위에서 'parquet 데이터 저장 할 때는 정렬한 후에 저장'하라고 썼는데,

이 때 저장을 어떻게 하느냐에 따라 predicate pushdown 성능이 달라질 수 있음

 

Z-ordering 이라는 정렬 방법을 사용하면, 여러 컬럼을 동시에 정렬 기준으로 삼을 수 있음

그리고 parquet 등이 갖는 메타데이터를 이용하여 (predicate pushdown 을 통해) IO 를 줄일 수 있음

즉, '안 읽어도 되는 파일'은 읽지 않음

 

예를 들어, 일반적인 정렬 (ORDER BY date, region) 을 진행하고 저장하는 경우

File 1: (1일, A), (1일, B), (1일, C), (1일, D)
File 2: (2일, A), (2일, B), (2일, C), (2일, D)
File 3: (3일, A), (3일, B), (3일, C), (3일, D)
File 4: (4일, A), (4일, B), (4일, C), (4일, D)


만약 WHERE region = 'A' 같은 쿼리를 날리면

메타데이터를 통해 모든 파일에 다 A 가 존재하는 것을 보고 모든 파일을 다 열어보게 됨 (=full scan)

File 1: region (metadata Min: A, Max: D) -> "A가 구간 안에 있네? 읽어야 함"
File 2: region (metadata Min: A, Max: D) -> "A가 구간 안에 있네? 읽어야 함"
File 3: region (metadata Min: A, Max: D) -> "A가 구간 안에 있네? 읽어야 함"
File 4: region (metadata Min: A, Max: D) -> "A가 구간 안에 있네? 읽어야 함"


Z-Ordering (ZORDER(date, region)) 으로 저장하는 경우

데이터를 'Z'자 모양의 곡선을 그리며 순서대로 번호를 매겨 저장하게 됨

File 1: (1일, A), (1일, B), (2일, A), (2일, B) -> [1~2일, A~B지역 모음]
File 2: (1일, C), (1일, D), (2일, C), (2일, D) -> [1~2일, C~D지역 모음]
File 3: (3일, A), (3일, B), (4일, A), (4일, B) -> [3~4일, A~B지역 모음]
File 4: (3일, C), (3일, D), (4일, C), (4일, D) -> [3~4일, C~D지역 모음]

 

WHERE region = 'A' 같은 쿼리를 날리면
메타데이터를 통해 몇몇 파일에만 A 가 존재하는 것을 보고 필요한 파일들만 열어보게 됨

File 1: region (Min: A, Max: B) -> "A가 구간 안에 있네? 읽음"
File 2: region (Min: C, Max: D) -> "A가 구간 밖에 있네? 안 읽음"
File 3: region (Min: A, Max: B) -> "A가 구간 안에 있네? 읽음"
File 4: region (Min: C, Max: D) -> "A가 구간 밖에 있네? 안 읽음"

 

이렇게 보면 효율이 좋지만, 반대로 생각하면, 날짜 기준으로 쿼리했을 때 성능이 떨어진다는 단점이 있음

z-ordering 을 고려해야하는 상황은, 다양한 컬럼으로 where 조건을 걸고 검색하는 경우가 잦을 때.

 

 

 

Z-Ordering 와 파티셔닝과 차이점 :

  - 파티셔닝은 디렉토리를 물리적으로 쪼개는 것

  - Z-Ordering은 파일 내부의 데이터를 재배치하여 메타데이터(Min/Max)의 효율을 극대화하는 것

 


파티셔닝을 날짜, 지역으로 설정한 상황을 가정해보자.

아래와 같이 파티셔닝 되어있을 것임

s3://my-data-lake/fact_sales/
  ├── date=2025-10-22/
  │     ├── region=seoul/
  │     │     ├── data_file_1.parquet
  │     │     └── data_file_2.parquet
  │     └── region=vancouver/
  │           ├── data_file_3.parquet
  ├── date=2025-10-23/
  │     ├── region=seoul/
  │     └── region=vancouver/

 

여기서 '나는 지역으로만 필터링하고 싶다!' 라면, 모든 데이터를 다 검색해야 함....

 

 

partitioning 과 z-ordering 을 함께 사용하면 IO 를 획기적으로 줄일 수 있겠지...

즉, 조건절에 자주 등장하는 컬럼 기준으로 partitioning 하고

그 partition내부 파일들을 z-ordering 하는 방식.

 

 

 

 


 

< 데이터 모델링 실습 : 은행 거래 데이터 >

 

 

 

0. raw data : 은행 간 계좌이체 진행시 기록되는 로그 데이터

trans_id,timestamp,sender_id,sender_name,receiver_id,amount,trans_type,bank_code,bank_name,status
TXN_550,2026-05-05 10:30:00,user_kim,김건우,user_lee,50000,폰뱅킹,088,신한은행,성공
TXN_551,2026-05-05 11:00:15,user_park,박지성,user_kim,120000,ATM,004,국민은행,성공
TXN_552,2026-05-05 11:15:20,user_lee,이강인,user_choi,15000,앱결제,090,카카오뱅크,실패

 

1. 비즈니스 프로세스 선택 (Select the Business Process)
이 파트에서는 'raw 데이터로부터 어떤 집계를 할 수 있을까' 하는 의문에 답을 해 봄

계좌이체 데이터가 있으니, '흠, 시간대별 자금이 어떻게 흐르는지 확인할 수 있겠군',

'아니면 은행 간 거래 비중이 얼마나 되는지 볼까?',

'혹은 송금 실패율을 확인해볼까?' 등을 꼽아볼 수 있음

그래서 비즈니스 프로세스는 '자금 이체(Fund Transfer) 이벤트'

2단계: grain 설정 (Declare the Grain)

이 파트에서는 가장 낮은 수준으로 fact table 의 row 를 구성할 수 있도록 설정함

즉, "한 명의 사람이 하나의 방법으로 다른 한 사람에게 이체한 기록 한 건" 처럼 가장 작은 단위를 grain 으로 설정할 수 있음

grain 은 이렇게 설정해두고, '일일 송금 한도'나 '월간 총 이체액' 같은 건 나중에 집계해서 보게 됨

 

3단계: 차원 식별 (Identify the Dimensions)

고정값을 갖는 dimension 으로 그룹지을 수 있는 컬럼들을 묶어서 dimension table 로 만듦
날짜시간(Date/Time), 보낸이(Sender), 받는이(Receiver), 거래 수단(Transfer Method), 은행정보(Bank), 거래 상태(Status)
여기서 보낸이, 받는이는 하나의 dimension table 로 처리 가능하기 때문에, Role-playing Dimension table 로 사용 가능

 

4단계: 사실 식별 (Identify the Facts)

분석의 대상이 되는 '측정 가능한 수치값'들을 여기에 넣으면 됨

이체 금액(Amount), 거래 소요 시간(Latency), 수수료(Fee)

상식적으로 생각해서, dimension table 에 들어가면 길이가 엄청 길어지겠다..싶은 것들은 fact table 에 남겨둬야 함

이체금액 정보 같은 데이터는 계속해서 생성되는 정보니까 당연히 fact table 에 넣어야 함

5. 이를 통해 만들어 본 fact table

date_key (FK),sender_key (FK),receiver_key (FK),bank_key (FK),method_key (FK),amount (Fact),fee (Fact),status
20260505,1001 (강하지),1002 (이강인),88,1,50000,500,1(성공)
20260505,1003 (박지성),1001 (정승범),4,2,120000,0,0(실패)

 

date 는 왜 dimension table로 만드는걸까?

아래와 같이, 미리 연/월/일/요일/분기 등 다양한 시간 속성을 미리 만들어두면 쿼리 성능과 편의성이 크게 향상되기 때문

CREATE TABLE dim_date (
    date_key        INT NOT NULL PRIMARY KEY,  -- 고유키 (예: 20230101)
    full_date       DATE NOT NULL,             -- 실제 날짜 (YYYY-MM-DD)
    year            INT NOT NULL,              -- 연도
    quarter         INT NOT NULL,              -- 분기
    month           INT NOT NULL,              -- 월 (1~12)
    month_name      VARCHAR(15) NOT NULL,      -- 월 이름 (January, 1월 등)
    day_of_month    INT NOT NULL,              -- 일 (1~31)
    day_of_week     INT NOT NULL,              -- 요일 (1=일, 7=토)
    day_name        VARCHAR(15) NOT NULL,      -- 요일 이름 (Monday, 월요일 등)
    is_weekend      BOOLEAN NOT NULL           -- 주말 여부
);

 

 


 

 

 

< 데이터 모델링 실습 : 광고 클릭/노출 로그 >

 

 

 

0. raw data : 광고가 노출/클릭될 때 기록되는 로그들

event_id,timestamp,user_id,ad_id,campaign_id,site_url,placement,action_type,cost_model,cost_amount
log_001,2026-05-05 12:00:01,user_88,ad_vancouver_01,CAMP_01,blog.naver.com,상단배너,impression,CPM,0.5
log_002,2026-05-05 12:00:05,user_88,ad_vancouver_01,CAMP_01,blog.naver.com,상단배너,click,CPC,150.0
log_003,2026-05-05 12:05:10,user_12,ad_tech_05,CAMP_02,youtube.com,영상중간,impression,CPV,1.2

 

 

1. 비즈니스 프로세스 선택 (Select the Business Process)
'raw 데이터로부터 어떤 집계를 할 수 있을까' 하는 의문에 답을 해 봄
해당 데이터는 광고가 노출 및 클릭 될 때마다 기록되니, '시간대별, 일별, 월별 광고 클릭률(ctr)을 계산해볼 수 있겠다',
'아니면 어느 위치의 광고가 가장 많이 클릭되는지 집계해볼까?' 등을 생각해볼 수 있음

그래서 비즈니스 프로세스는 '광고 성능 측정' 혹은 '광고 성과 측정(Ad Performance Tracking)'

 

2. Grain 설정

가장 낮은 단위로 진행해야하므로

'한 사람이 하나의 광고를 하나의 디바이스에서 하나의 위치에서 한 번 클릭/노출한 단위'를 grain 으로 설정

위 raw data 그대로 사용해도 될 것 같음

 

3. dimension 식별

묶을 수 있는 것들을 묶어보자.

날짜별로 묶을 수 있고, 유저 정보로 묶을 수 있고..(근데 이 raw 에는 유저정보가 별로 없어서 묶어도 큰 의미는 없을 듯)

광고 정보로 묶을 수 있고, 광고 매체로 묶을 수 있고, 캠페인 정보로 묶을 수 있음

여기서 '캠페인'이라는 개념을 알고 있어야 dimension 으로 묶이겠구나 라고 판단 가능

따라서 비즈니스를 잘 이해하고 있어야 함

 

dim_ad_creative: 광고 소재 정보 (소재Surrogate Key, 광고주명, 배너사이즈, 카테고리 등)
dim_campaign: 캠페인 정보 (캠페인SK, 캠페인명, 시작일, 종료일, 목표 등)
dim_publisher: 매체 정보 (매체SK, 사이트URL, 도메인 카테고리 등)
dim_date: 날짜 정보 (날짜SK, 연, 월, 일, 분기, 요일 등)

 

 

4단계: fact 식별

분석의 대상이 되는 '측정 가능한 수치값'들은
클릭 횟수, 노출 횟수, 비용 등.

클릭/노출 될 때마다 쌓이게 되니까 fact table 에 있어야 함

(고정 비용이라면 dimension 에 있어도 될 것 같은데, 고정 비용이 아니라면 fact table 에 있어야 할 듯)

 

 

5. 아래와 같은 fact table 을 만들어 봄

date_key (FK),ad_key (FK),campaign_key (FK),publisher_key (FK),is_impression,is_click,cost (Fact)
20260505,501,10,901,1,0,0.5
20260505,501,10,901,0,1,150.0
20260505,702,11,905,1,0,1.2

 

여기서 노출 및 클릭을 따로 컬럼으로 만들어서 처리했음

is_impression, is_click 처럼 0 또는 1의 값을 갖는 컬럼을 만들면

나중에 SUM(is_click)만으로도 전체 클릭 수를 아주 빠르게 계산할 수

물론 CTR(클릭수/노출수 * 100)계산에도 용이함

 

CTR 같은 지표는 Fact 테이블에 미리 저장해두지 않고, 필요할 때마다 그때 그때 집계해서 사용함

 

 

 

 


 

 

 

< 데이터 모델링 실습 : 제조업 또는 물류 센터의 재고 흐름(Inventory Flow) >

 

0. raw data :

txn_id,event_time,warehouse_id,location,product_id,product_name,txn_type,quantity,unit_cost,worker_id
T-901,2026-05-06 09:00:00,WH_VAN,Vancouver_A1,SSD_1TB,삼성 SSD,입고(In),100,80000,eng_hong
T-902,2026-05-06 10:30:00,WH_VAN,Vancouver_A1,SSD_1TB,삼성 SSD,출고(Out),20,80000,eng_hong
T-903,2026-05-06 11:00:00,WH_SEO,Seoul_B2,RAM_16G,하이닉스 램,입고(In),50,60000,user_kim

 

 

 

 

 

 

 

 

 

'Coding Interview' 카테고리의 다른 글

[IT] dbt 공부  (0) 2026.05.22
[Spark] interview questions  (0) 2026.01.22
[Data Engineer] interview questions  (0) 2026.01.16
[Airflow] interview questions  (0) 2025.12.14
[SQL] interview questions  (0) 2025.12.12

 

개발 영어 공부1 : https://eyeballs.tistory.com/683

 

 

Spark was compiled for 2.13 스파크는 2.13 용으로 컴파일되었다.
It determines when to remove and request executors. 결정하다.
If the jobs at the head of the queue don't need to use the whole cluster, later jobs can start to run right away 대기열의 맨 앞에 있는 jobs
후속 jobs
then later jobs may be delayed significantly
significant computing resources may be required.
상당히 지연될 수 있다.
상당한 컴퓨팅 자원이 요구될 수 있다.
This feature is disabled by default. 기본적으로 비활성화되어있다.
Newly submitted jobs go into a default pool. 새롭게 제출된 작업들은 default pool에 들어간다.
Jobs run in FIFO order
Jobs run in order.
Automate A, B, and C in order of importance.
data is stored in chronological order
FIFO 순서로 작업이 실행된다.
순서대로 작업이 실행된다.
중요한 순서대로 자동화하라
시간순으로 저장되어있다
both of these together make up the node
Distributed storage refers to a storage architecture made up of multiple computers and disks.
노드를 만든다.
다양한 컴퓨터와 디스크로 구성된 저장 아키텍처
The same is true of the C node. C노드도 마찬가지다.
Our managers deal with all kinds of clients every day 매일 다양한 clients 를 상대한다.
In my previous project I carried out the responsibilities of... 나의 이전 프로젝트에서, 이러이러한 책임을 수행했다.
Project managers usually estimate new projects by analogy 유사사례를 통해 신규 프로젝트를 추정한다.
I improved my time management and organizational skills. 시간 관리 및 시간 정리 능력을 향상시켰다.
the server performs instructions written in code. 명령을 수행한다.
use only the numbers in given array.
Cardinality refers to the number of unique values in a given dataset.
주어진 배열 내 숫자만 사용하라
카디널리티(Cardinality)는 특정 데이터 집합에서 고유한 값(unique values)의 개수를 의미한다.
assigned the number 33 to age variable. 나이 변수에 33을 할당했다.
I created an array of strings. 문자열 배열을 생성했다.
Debugging is to investigate the program and fix bugs. 프로그램을 조사하는 것이다.
A program crashes when you divide by zero. 프로그램이 중단된다.
declared a function and implemented it. It works well!  함수 하나를 선언했고 구현했다. 잘 동작한다.
instantiated another object of the Student class. 또 다른 학생 클래스 객체를 인스턴스화했다.
iterated(loop) through every element in the list. 리스트 내 모든 요소를 순회했다.
Processing is manipulation of data by a computer 컴퓨터에 의한 데이터 조작이다.
Raw data can be converted into JSON format. Josn 형태로 변환될 수 있다.
Maintainability : It should be stable when the changes are made 변경사항이 적용되었을 때 안정적이어야 한다.
You need a dedicated work space 전용 작업 공간
Let's move our meeting back an hour. 한 시간 미루자
She moved the meeting up a day. 미팅을 하루 앞당겼다.
The meeting was rescheduled for Thursday. 회의가 목요일로 재조정되었다.
From a subjective perspective. 주관적인 관점에서
We will meet in a week and synchronize on the progress. 진행 상황을 맞춰보자.
Big data enables you to gather data from many sources. 데이터를 수집할 수 있게 한다.
A kafka topic is identified by its name
Each broker is identified with its ID.
카프카 토픽은 이름으로 식별된다.
The sequence of messages is called a data stream
Sequential messages
received messages in sequence.
processing data sequentially is necessary
메세지의 순서를 데이터 스트림이라고 부른다.
연속된 메세지들
순서대로 수신한 메세지들
연속으로 데이터를 처리하는 것이 필요하다.
Topics are split in partitions. 토픽은 파티션으로 나뉘어진다.
Each message gets an incremental id, called offset.
'Append' adds only the newly arrived data incrementally
하나씩 증가하는 id
'Append' 는 새로 추가된 데이터를 증분으로 추가한다.
Kafka topics are immutable 토픽들은 불변한다.
Data is kept only for a limited time.
Data is maintained only for a limited time.
Data remained only for a limited time.
데이터는 제한된 기간 동안만 보관된다.
데이터는 제한된 기간 동안만 유지된다.
데이터는 제한된 기간 동안만 남아 있는다.
Over time, the kafka clients have been changed. 시간이 지남에 따라...
The processing of rapidly increasing data is entrusted to Hadoop.
I delegated the task to Hadoop
빠르게 증가하는 데이터 처리를 하둡에게 맡긴다.
하둡에게 그 일을 위임했다.
There are two main methods for data transmission.  
The "streaming type" continuously transmits data as it is generated, in real time. 실시간으로 생성되는 데이터를 끊임없이 보내는 방법이다.
Data collected over the past 30 minutes is aggregated
If you want to graph the event trend over the past hour..
지난 30분동안 모은 데이터들이 집계되었다.
과거 1시간의 이벤트 수 추이를 그래프로 만들고 싶으면..
To analyze data from the past 3 years 지난 3년간의 데이터를 분석하기 위해
Amazon S3 is a representative example of object storage. 대표적인 객체 스토리지다.
it can increase data capacity in the future. 향후 데이터 용량을 증가시킬 수 있다.
Most people are accustomed to using SQL SQL 사용에 익숙하다.
This series of procedures is called the ETL process 일련의 절차를 ETL 프로세스라고 부른다.
Batch processing is executed according to a fixed daily schedule 고정된 일일 일정에 따라 실행된다.
If an error occurs, the administrator is notified. 관리자에게 알려진다.
System failures can occur during big data processing.
Therefore, it is necessary to implement functions to handle and retry tasks when errors occur.
처리하는 기능을 구현하는 것이 필수다.
Data collected throughout the day is aggregated overnight to generate reports. 하룻동안 수집된 데이터들은 밤사이 집계된다.
Unprocessed raw data is stored in a data lake, and only the necessary data is retrieved and used later. 미가공한 원시 데이터가 data lake 에 저장된다.
그리고 필요한 데이터만 추출된다.
Data engineers are responsible for building, managing, and automating the systems. 데이터 엔지니어는 책임을 진다.
It’s best to start with a small system and gradually scale it up over time. 나중에 단계적으로 확장해가는 것이 좋다.
In the early stagesmanually collecting and analyzing data without automation is called ad hoc analysis. 시작 단계에서..
Searching for data that meets specific criteria  특정 기준을 충족하는 데이터 검색하기
values in the 'time' column can be tricky to handle time 컬럼값은 다루기가 까다롭다
The same aggregation is repeated on a regular schedule—weekly or monthly—to observe trends over time. 1개월 혹은 1주일마다 정기적인 일정으로 동일한 집계를 반복하고 그 추이를 관찰한다
the number of ad impressions. 광고 노출 수
The number of customers who used the service in a day.
The number of customers who used the service within a day.
하루에 서비스를 이용한 고객 수 (하루 전체. 통으로)
하루 이내에 서비스를 이용한 고객수 (24시간 이내에)
Making decisions based on objective data is called data-driven decision making. 객관적인 데이터를 근거하여 판단하는 것을 '데이터 기반 의사 결정'이라고 한다.
the values in A column are generally higher/lower/similar to the values in B column. 전체적으로
As long as you have organized numerical data 정리된 숫자 데이터가 있다면
names are listed in rows, and sales are listed in columns. 행 방향으로 나열되고, 열 방향으로 나열된다.
the numeric data appears at the intersections of rows and columns 행과 열의 교차점
large datasets that can't fully fit in memory
Load 
all the data into memory.
메모리에 다 들어가지 못하는 큰 용량의 데이터
모든 데이터를 메모리에 올린다.
If the wait time for aggregation increases, all processes slow down. 대기시간이 늘어나면, 모든 작업들이 느려진다.
your system needs to be designed for it from the start. 처음부터 그렇게 디자인되어야 했다.
fast data processing is described as having low latency. '지연시간(latency)이 적다'고 표현된다.
a sharp drop/improvement in performance 급격한 성능 저하/향상
The amount of data that can be processed within a certain period of time is called "throughput". 일정 시간 내에 처리할 수 있는 데이터의 양을 처리량(throughput)이라 한다.
The time you wait for data processing to complete is called "latency". 작업이 끝날 때 까지 대기하는 시간을 지연시간(latency)이라 한다.
The presence or absence of indexes
Which option to choose depends on the situation.
인덱스 유무
어떤 옵션을 선택할지는 상황에 따라 달라진다.
a query is broken down into many smaller tasks 다수의 작은 태스크로 분해된다.
you need to scale both CPU and disk resources in a balanced way. 균형있게 자원을 늘려야한다.
exploring the data through repeated trial and error. 반복적인 시행착오를 통해
In contrast to ad hoc analysis, you can run queries on a regular schedule to generate reports. ad hoc 분석과 대조적으로..
you want to take your time and carefully examine the data. 시간을 갖고 차분히 데이터를 보고 싶다.
Metric B is updated once a day. B 메트릭은 하루에 한 번 업데이트 된다.
improvements in computing power 컴퓨팅 성능 향상
There are more/fewer cases now where building a data mart is unnecessary. 이러한 경우가 늘어나고/줄어들고 있다.
Normalization involves splitting tables as much as possible and linking them using foreign keys.  
Denormalization involves combining tables as much as possible.  
tables are categorized into fact tables and dimension tables.  
Fact tables store data that accumulates over time. 시간에 따라 증가하는 데이터가 저장된다.
Dimension tables typically store attributes used to categorize the data.  
A model where a fact table is surrounded by multiple dimension tables is called a star schema.  
Files are replicated across multiple machines to increase redundancy. 중복성을 높이기 위해 파일을 복사한다.
jobs are preferably executed on nodes close to the data. 가급적, 데이터와 가까운 노드에서 실행한다.
spontaneously
simultaneously
자발적으로
동시에
resource contention(competition) occurs between jobs 자원 쟁탈(경쟁)
they run only when no one else is using the resources. 누구도 자원을 사용하지 않을 때만
Spark keeps intermediate data in memory
It is safer to process data in an intermediate table first 
중간 데이터를 메모리에 보존한다.
중간 테이블을 만들어 처리하는 게 안전하다.
problems arise when aggregating large amounts of data over a long period. 장기간에 거쳐
data is evenly distributed across all nodes 균형있게 분산되어있다.
storage and compute nodes are tightly coupled 밀접하게 결합되어있다.
it either waits for resources to free up or fails with an error. 메모리가 생길 때까지 기다리거나
memory usage doesn’t increase proportionally 비례하여 늘어나지 않는다.
memory consumption remains nearly constant 메모리 사용량은 거의 일정하게 유지된다
You can aggregate data with millions of records in under one second 수백만 레코드를 갖는 데이터를 1초 미만으로 집계할 수 있다.
Even if a partial failure occurs, processing can continue as a whole 부분적으로 장애가 발생해도, 전체적으로 처리를 계속할 수 있다.
Tez is a replacement for MapReduce and inherits its fault tolerance.
Tez is an alternative to MapReduce.
Tez 는 MapReduce 를 대체하는 것이며, 그 내결함성을 계승하고 있다.
Presto is the complete opposite of Hive presto 는 hive 와 완전히 반대입니다
Presto is specialized for executing interactive queries. Presto 는 대화식 쿼리의 실행에 특화되어 있다.
excessive usage can prevent other queries from running.
Excessively reducing cardinality can lead to significant information loss
무리한 사용
카디널리티를 무리하게 낮추면 원래 있던 정보가 크게 손실된다
it handles schema changes more flexibly. 스키마 변동에도 유연하게 대처할 수 있다.
frequent read/write operations on small amounts of data 빈번하게 소량의 데이터를 읽고 쓰는 것
It increases the rate of unexpected errors.
the likelihood of unexpected errors increases.
예상치 못한 오류 발생률을 높인다.
It's a design issue rather than a performance problem.
Massive data should be divided and processed in parts rather than all at once
성능 문제라기보다는 설계 문제이다.
한 번에 처리하기보다는 부분으로 나눠서 처리해야한다.
What is the optimal file size for efficient processing? 효율적으로 처리할 수 있는 파일이 크기는 얼마나 될까?
These two differ entirely in both technical characteristics and tools used, so you must understand their nature and use them accordingly. 이 둘은 기술적인 특성도, 사용되는 도구도 전혀 다르므로 그 성질을 이해한 다음에 구분해서 사용해야 한다.
When handling large volumes of data, break tasks into monthly or daily units to prevent any single task from becoming too large. 한 달 혹은 하루 단위로 전송하도록 태스크를 분해하여, 너무 커지지 않도록 막는다.
Fluentd only sends messages in one direction 일방적으로 발송하는 것밖에 하지 못한다
mobile apps often go offline
the device is back online.
모바일 앱은 오프라인이 되는 경우가 종종 있다.
다시 온라인 상태가 된다.
It can focus solely on its own tasks while leaving the rest to the shared system. 작업에만 오롯이 전념할 수 있고, 나머지 작업은 공통 시스템에 맡길 수 있다.
the two are in a trade-off relationship 둘은 트레이드오프 관계에 있다.
you just reached a performance limit 성능 한계에 도달했다.
regulate the data write rate. 데이터 쓰기 속도를 조절하라
you need to decide in advance how to operate the system in its absence. 그것이 없을 때 어떻게 시스템을 운용할지 미리 결정해야한다.
Only keep the IDs received within the last hour, and allow duplicates that arrived later.
Allow duplicates for late-arriving messages.
최근 1 시간 동안 받은 ID 만 기억해두고, 그보다 늦게 온 메세지의 중복은 허용한다.
The time a message is generated on the client is called “event time,”  (on the client side)  
the time the server processes the message is called “process time.”  
the data is arranged in a contiguous layout. 데이터가 연속적으로 배치되어있다.
Eventual consistency guarantees that all replicas will eventually converge to the same value over time. 시간이 지나면 결국 동일한 값으로 귀결된다.
Strong consistency guarantees that all read operations reflect the most recent write. 가장 최근의 쓰기를 반영한다.
a human intervenes to resolve the issue. 사람이 개입하여 문제를 해결한다.
Some tasks can cause new problems if they are not completed by the scheduled time 예정된 시간까지 끝내지 않으면 새로운 문제를 일으키는 태스크도 있다.
Let's finish it within the allotted time. 정해진(할당된) 시간 내에 끝내자.
There are tools that notify you when a task exceeds its expected execution time. 작업이 예상 실행 시간을 초과할 때 알려주는 도구가 있다.
It is important to anticipate potential unexpected errors in advance 예기치 못한 오류 발생 가능성을 예상하는 것은 중요하다.
Setting the retry interval of a task to 10 minutes 작업의 재시도 간격을 10분으로 설정
“Backfill” refers to rerunning tasks over a specific period by changing the date parameter in sequence. ‘백필’이란 파라미터에 포함된 날짜를 순서대로 바꿔가면서 일정 기간의 태스크를 다시 실행함을 의미한다.
You can test backfill gradually 테스트 삼아 조금씩 백필을 실행할 수 있다.
In Airflow, scripts must be written with atomicity and idempotency Airflow 에서는 원자성과 멱등성을 갖춘 스크립트를 작성해야 한다.
it helps improve stability. 안정성을 높인다.
An execution with this property is called an idempotent operation.
이런 특성을 지닌 실행을 idempotent operation 이라고 한다.
'replacement' yields the same result even when repeated '치환'은 반복해서 실행해도 동일한 결과를 산출한다.
Increase the number of retries while gradually expanding the interval between them 재시도 횟수를 늘림과 동시에, 조금씩 재시도 간격을 넓혀나가라
automatic retries must be disabled, and recovery should be done manually in case of failure.
자동 재시도는 반드시 무효로 하고, 오류 발생 시 수작업으로 복구한다.
Adjust each task so that it has an appropriate size. 각 태스크가 적절한 크기가 될 수 있도록 조정한다.
nodes are connected by arrows, 
and these connections never form cycles.

노드와 노드가 화살표로 연결되며
각 노드 연결이 순환되지 않는다.
We can reprocess data starting from 7 days ago. 7일 전 데이터부터 재처리할 수 있다.
fast-response database is placed in the serving layer 서빙 레이어에 응답이 빠른 데이터베이스를 설치한다
Since the results of stream processing are used only temporarilyslight inaccuracies are acceptable. 스트림 처리의 결과는 일시적으로만 사용되며, 정확하지 않아도 큰 문제가 없다.
Handling messages with a large gap between processing time and event time is referred to as the out-of-order data problem. 프로세스 시간과 이벤트 시간의 차이가 큰 메세지를 처리하는 것을 ‘out of order’ 데이터 문제라고 불린다.
they must be sorted by event time before aggregation 이벤트 시간 순서로 정렬하고 집계한다.
Therefore, the system must retain the state of past events 
and re-aggregate the corresponding window whenever new data arrives.
때문에 과거 이벤트의 상태를 보존하면서, 데이터가 도달할 때마다 해당 윈도우를 재집계한다.
Since data cannot be stored indefinitely
data that arrives too late beyond a certain time threshold is ignored.

데이터를 무한히 계속 보관할 순 없으므로 일정 시간 이상 늦게 온 데이터는 무시한다.
In ad hoc data analysis, interpreters are preferred. ad hoc 데이터 분석에서는 인터프리터를 선호한다.
Each tweet is formatted as JSON data with a variable length.
각 트윗은 길이가 일정하지 않은 json 데이터로 되어 있다.
Virtual machines allow the entire team to share a consistent setup 팀 전원이 같은 환경을 공유할 수 있게 해준다.
extracting data for a specified period
The error occurred under the specified conditions.
명시된 기간 만큼의 데이터 추출하기
명시된 조건에서 발생했다.
overwriting a designated partition 지정된 파티션 덮어쓰기
As long as the parameters remain the same 파라미터만 같다면
The functions executed at this time are serialized and lazily evaluated 이 때 실행되는 함수는 직렬화되어 지연 평가된다.
If the schedule is set to @daily, the task for January 1st is executed at the moment January 2nd begins.
there is a one-day gap 
스케줄이 @daily 라면, 1월 1일의 태스크가 실행되는 것은 다음 날 1월 2일이 되는 순간이다.
1일의 차이가 있다.

if more tasks are queuedthey are put on hold until slots become available. 그 이상의 태스크가 등록되면 빈 자리가 생길 때까지 실행이 보류된다.
Adjust task durations so that each one doesn’t take too long 각 태스크의 실행 시간이 길어지지 않도록 조절하라
It hasn't been that long. 그렇게 오래되진 않았어.
keep the system in a relaxed state.
항상 여유 있는 상태를 유지하라
By automating what was previously a manual reprocessing task 이전에 수동으로 처리해야 했던 작업을 자동화함으로써
a single re-run handles all necessary steps 필요한 모든 과정을 처리했다.
If I were asked to handle tuning, my approach would be:
first, measure and identify the bottleneck by profiling,
then research best practices or consult documentation,
and validate improvements step by step.
 
I’d definitely want to confirm the original intent before making any changes. 반드시 원래 의도를 확인하겠다.
I was tasked with building a data pipeline 데이터 파이프를 구축하게 되었음
To ensure idempotency, it deletes existing data for each date and regenerates it
날짜 단위로 기존 데이터를 삭제하고 새로 데이터를 만듦
there was no process in place to verify data quality.
데이터 품질 검사를 위한 프로세스가 마련되어 있지 않았다.
I identified and resolved the root causes of the inconsistency 원인을 발견하고 해결하였다.
The source team’s table had a different primary key (PK) configuration, causing duplicate records. 데이터 복제를 발생시켰다.
Some data was missing because their encryption logic malfunctioned during transmission
암호화 로직이 잘못 적용되어있었다.
we began receiving accurate data consistently.
정합성있는 데이터를 받을 수 있게 되었다.
I carried out a data cleansing process to remove or obscure PII where it would not affect business decisions.
PII 를 제거하거나 가리기 위해 클렌징 작업을 수행했다.
I masked the middle part of names with stars to obfuscate the PII. 모호하게 하기 위해 이름 중간 부분을 별표로 가렸다.
I applied a retention policy to PII, performing a soft delete for any PII older than three months from the current date. PII 에 retention policy 를 적용하여 현재 날짜로부터 3개월이 지난 모든 PII에 대해 소프트 삭제를 수행했다.
I checked cross-version backward compatibility within the EMR environment to ensure there would be no issues after the upgrade. 업그레이드 후 문제가 발생하지 않도록 EMR 환경 내에서 버전 간 하위 호환성을 확인했다.
I then provisioned a new EMR cluster with the latest version 그런 다음 최신 버전으로 새 EMR 클러스터를 프로비저닝했다.
There were KTLO (Keep The Lights On) jobs scheduled by time, similar to crontab
crontab 처럼, 시간으로 스케줄링 해 둔 KTLO 작업들이 존재함
if an upstream job failed, it caused downstream failures.
상위 작업이 실패하면 하위 작업의 실패를 초래했다.
I modified the code to be idempotent 코드를 멱등성을 갖도록 수정했다
This reduced human error and improved operational efficiency. 인적 오류도 감소하고 운영 효율성이 향상되었다.
I will proactively seek out and learn technologies I haven't used before. 사용해 보지 않은 기술을 적극적으로 찾아 배울 것이다.
I will invest time to take online courses, research best practices, and get hands-on experimentation. 온라인 강좌 수강, 모범 사례 연구, 그리고 직접적인 실험을 위해 시간을 투자할 것임
I will ensure no disruption to my work. 업무에 차질이 없도록 하겠다.
work with BA and DS teams, and respond to their requests.
업무요청 대응하기
I took over the task of synchronizing data 데이터 동기화 작업을 인수인계 받았다
Occasionally, incorrect data would come in from the source table 가끔씩 소스 테이블에서 잘못된 데이터가 들어오곤 했다
we only found out days later. 우리는 며칠이 지나서야 알게 되었다.
he told me to retrieve the data for that date again 
and move it to the target table.
그 날짜의 데이터를 다시 가져와서 옮기라고 말했다.
arbitrarily moving past date data would break consistency. 임의로 과거 날짜 데이터를 이동하면 일관성이 깨질 수 있다.
I learned that rather than just following instructions blindly,
I must double-check whether my work is correct
맹목적으로 따라하지 말고, 내가 하는 일이 잘 하는게 맞는지 재확인
To minimize risk, I created isolated test resources like a dedicated EMR cluster. 전용 EMR cluster 같이, 분리된 테스트 리소스를 생성했다
Once validated, I safely switched over  테스트가 마무리 된 이후 새로운 테이블을 기존 테이블로 안전하게 교체하였다
All team members wanted to switch directly to Spark 3 without testing  
This could lead to a dangerous situation where the entire operation halts 이는 전체 작업이 중단되는 위험한 상황으로 이어질 수 있다.
once I was assigned to handle WebUI processing using Spring.
I was assigned the task of adding functionality to display different screens based on user permissions
Spring을 사용한 WebUI 처리를 담당하도록 배정받았다.
사용자 권한에 따라 다른 화면을 표시하는 기능을 추가하는 작업을 맡게 되었다.
I conducted code reviews with team members to double-check for any shortcomings. 부족한 점이 없는지 다시 한번 확인했다.
I performed a staging deployment to verify that the screens displayed correctly for users with different permissions. 화면이 올바르게 표시되는지 확인하기 위해 스테이징 배포를 수행했다.
I received negative feedback regarding time management. 시간 관리에 대해 부정적인 피드백을 받았다.
I missed the deadline while waiting for approval and
received feedback that I failed to meet timelines.
승인을 기다리다 마감일을 놓쳤고
일정을 지키지 못했다는 피드백을 받았다.
I need approval to proceed with my work
I applied for approval in advance and requested it multiple times
but the response came late

업무 진행을 위해 승인이 필요하다.
사전에 승인을 신청하고 여러 차례 요청했다
하지만 답은 늦게 도착했다.
I will be comfortable working independently and handling tasks reliably.
독립적으로 업무를 수행하고 신뢰할 수 있게 업무를 처리하는 데 익숙해 질 것이다.
I want to participate in decisions about future direction and contribute to the team's future. 향후 방향에 관한 결정에 참여하고 팀의 미래에 기여하고 싶다.
My biggest strength is identifying inefficiencies in systems and improving them.
 
I’ve developed a strong habit of documentation and logging.
 
I'll share my thoughts along with the materials I researched and gathered beforehand.
사전에 조사하고 수집한 자료와 함께 내 생각을 공유할 것이다.
ROW_NUMBER() assigns a unique sequential number to each row, regardless of whether the values are the same.
 값이 같은지 아닌지 상관 없이
DENSE_RANK() assigns the same rank to rows with the same value, and the next rank is assigned without gaps. 다음은 갭 없이 랭크가 부여된다
RANK() assigns the same rank to rows with the same value, but it leaves gaps in the ranking sequence after ties. 같은 값을 갖는 랭크 이후 갭이 있다
Window functions are functions that preserve the context of the original data and do not reduce the number of rows after aggregation.  집계 후에 원본 데이터 맥락을 유지한다.
JOIN combines columns horizontally based on a condition, 
while UNION combines rows vertically and requires schema compatibility.
 
EXISTS ignores null values because it determines whether a row exists. (The existence of a row)
EXISTS 는 행 존재 여부를 판단하기 때문에 null 을 무시함
but I understand the concept and have worked on similar problems  
I’m not familiar with that term, so I don’t want to give an incorrect explanation.  
If it’s something I’ve encountered before under a different term, I’d be happy to explain from that perspective.
혹시 제가 알고있는 개념이라면 보충해서 이야기를 할 수 있을 것 같다
I haven’t worked with Streaming System yet, so I wouldn’t want to explain it inaccurately.
 
If your team uses Streaming System in production,
I’d make sure to study by watching online lectures,
understand how I can use and why it was chosen here,
and get hands-on experience 
so I can work with it safely in a production environment.
 
haven’t had hands-on experience with performance tuning yet. However, I have similar experience in terms of tuning.  
I can’t say for certain what the exact reasoning was.
정확한 이유가 무엇이었는지 단정할 수 없다.
However, based on my understanding of the system,
one possible reason could be cost efficiency in this part
하지만 제가 이해한 시스템 구조상 이 부분에서는 비용 효율성이 한 가지 이유가 될 수 있다
I didn’t design it myself, so I’d avoid making assumptions.  
The requester wanted data processed with the desired transformation
and wished to receive the data via SFTP according to a set schedule.
Throughout this process, the requester and I discussed how to handle duplicate data,
the final number of files, 
and how to notify us if issues occur.
요청자는 원하는 변환 방식으로 처리된 데이터를 원했으며,
정해진 일정에 따라 SFTP를 통해 데이터를 수신하기를 원했다.
이 과정 전반에 걸쳐 요청자와 저는 중복 데이터 처리 방법,
최종 파일 수, 문제 발생 시 통보 방식에 대해 논의했다.
We conducted many tests and
completed the work as the requester wanted.
As a result, I successfully built the data pipeline,
and the requester received the desired data.
우리는 여러 차례 테스트를 수행하고
요청자가 원하는 대로 작업을 완료했습니다.
그 결과, 데이터 파이프라인 구축에 성공했으며
요청자는 원하는 데이터를 수령했습니다.
Just retrieve the data for the date the issue was created  이슈를 만든 날짜의 데이터를 찾아라.
I proceeded with the task that way 그 방식으로 작업을 진행했다.
Therefore, arbitrarily moving past date data would break consistency.
따라서 과거 날짜 데이터를 임의로 이동하면 일관성이 깨질 것이다.
We only discovered this much later.
그제서야, 나중에야, 뒤늦게서야...(아쉬움)
S3 doesn't have the concept of directories.  s3 는 디렉토리 개념이 없다
Durability refers to the potential for data loss.
Durability refers to the likelihood of not lossing data.
Availability refers to the probability of being able to read data without issues.
Durability : 데이터를 잃어버릴 가능성. 99.999999999%
Availability : 데이터를 문제없이 읽을 수 있는 확률
We can use tags as indicators for distinguishing pusrposes when settling accounts later.
s3 bucket 에 tag 를 달면, 나중에 정산할 때 구분을 위한 지표로 사용 가능함
Glue Crawlers parse and infer from the csv file
Glue Crawlers 는 이 csv 파일의 정보를 분석하고 추론함
It remembers how far it previously collected data and
resumes collection from the point where it stopped.
이것은 이전에 데이터를 수집한 지점을 기억하고
중단된 지점부터 수집을 재개한다.
There are spaces before and after the name.
고객명 앞뒤 공백 있음
   
   

 

 

 

 

Spark 이론 정리 : 

https://eyeballs.tistory.com/m/206

 

 

Spark 질문은 원리 + 실무 판단력을 보기 위함

Spark UI 언급 = 실무 경험 신뢰도 급상승

모든 질문에 완벽할 필요 없음

왜 그렇게 했는지 설명할 수 있으면 충분

 

 

 

What is Apache Spark and why is it used?

Apache Spark is a distributed data processing framework designed for large-scale data processing. 
It provides in-memory computation, fault tolerance, and a high-level API, 
which makes batch and iterative workloads much faster compared to traditional MapReduce.

 

What is the difference between RDD and DataFrame?

RDD is a low-level API that provides fine-grained control but lacks automatic optimization. 
DataFrames are higher-level, schema-aware, and benefit from Catalyst Optimizer and Tungsten execution engine, 
so they are generally preferred for most workloads.


 

What is lazy evaluation in Spark?

Spark does not execute transformations immediately. 
Instead, it builds a logical execution plan and only triggers computation when an action is called, 
which allows Spark to optimize the execution plan.

 

What is the difference between transformation and action?

Transformations define how data should be processed and are lazily evaluated, 
while actions trigger the actual execution and return results or write data.

 

Explain Spark’s execution flow.

When an action is called, Spark creates a job, which is divided into stages based on shuffle boundaries. 
Each stage consists of tasks that are executed in parallel on executors.


 

What is a shuffle and why is it expensive?

A shuffle involves redistributing data across executors, usually during joins or aggregations. 
It is expensive because it requires disk I/O, network transfer, and serialization.


 

What causes shuffle in Spark?

Operations like groupBy, reduceByKey, join, distinct, and repartition can trigger shuffle 
because they require data to be reorganized across partitions.


 

How do you optimize joins in Spark?

I try to reduce shuffle by using broadcast joins when one dataset is small enough. 
I also ensure proper partitioning and avoid skewed join keys when possible.


 

What is data skew and how do you handle it?

Data skew occurs when a few keys dominate the data distribution, causing some tasks to take much longer. 
Common approaches include salting keys, filtering hot keys, or using broadcast joins.

 

What is partitioning and why is it important?

Partitioning determines how data is distributed across executors. 
Proper partitioning improves parallelism and resource utilization, 
while poor partitioning can lead to performance bottlenecks.


 

What is the difference between repartition and coalesce?

repartition increases or decreases partitions and triggers a shuffle, 
while coalesce typically reduces partitions without a full shuffle.


 

When would you cache or persist data?

I cache data when it is reused multiple times across different actions, especially if the computation is expensive.
I choose the storage level based on memory availability.

 

How does Spark handle failures?

Spark uses lineage information to recompute lost partitions. 
If a task or executor fails, Spark retries the task automatically on another executor.

 

Why is Parquet commonly used with Spark?

Parquet is a columnar storage format that supports compression and predicate pushdown, 
which reduces I/O and improves query performance.

 

How do you debug a slow Spark job?

I start by checking Spark UI to identify slow stages or skewed tasks, 
then review shuffle size, partition count, 
and executor utilization before applying optimizations.


 

Spark execution model 종류 및 차이
 

 

Spark execution flow (논리적 모델, 물리적 모델, stage, task 등)
 

 

partition, parallelism. input 파일 개수와 partition 의 관계?
 

 

join 종류와 어떤 상황에서 어떤 join 을 선택해야 하는지
 

 

executor 실패시 어떻게 되는지, 어떻게 복구할건지
 

 

skew 발생시 어떻게 대응할지
 

 

Why is this spark job slow? 느려지는 경우 어디서부터 어떻게 원인 찾고 해결할래?
 

 

How would you optimize it? 튜닝 방법
 

 

What are the RDD's in Spark?
 

 

Spark UI 에서 확인해야 하는 포인트
https://eyeballs.tistory.com/715

< Jobs Tab >


- 몇 개의 job이 실행되었는지
- 어떤 action이 job을 트리거했는지

하나의 action 이 하나의 job 을 만듦. 꼭 기억하자




< Stages Tab >

- Stage 실행 시간
- Shuffle Read / Write 크기
- Failed / Skipped stages

Stage 경계(boundary) 는 shuffle 기준으로 나뉨



< Tasks 상세 화면 >

- Task 실행 시간 분포
- 특정 task만 유난히 느린지 (skew 발생 여부 확인)
- Input size 차이

몇몇 task 가 특출나게 오래 실행된다면, skew 를 의심해볼 수 있음



< SQL Tab (DataFrame 사용 시) >

- Physical Plan
- BroadcastHashJoin 여부
- SortMergeJoin 사용 여부

물리적 실행 계획을 보고 어떤 join 전략이 사용되었는지 확인할 수 있음



< Storage Tab >

- 캐시된 DataFrame/RDD
- 메모리 사용량
- Storage Level



< Executors Tab >

- Executor 수

- CPU/Memory 사용률
- Shuffle Read/Write per executor
- GC time

각 노드의 리소스가 고르게 쓰이고있는지 확인 가능함



면접에서 다음과 같이 말하면 좋음

When a Spark job is slow, I usually start with the Spark UI, especially the Stages and Tasks tabs,
to identify shuffle-heavy stages or data skew.




 

 

 

 

 

 

 

 

'Coding Interview' 카테고리의 다른 글

[IT] dbt 공부  (0) 2026.05.22
[IT] Data Warehouse + Data Modeling 공부  (1) 2026.04.16
[Data Engineer] interview questions  (0) 2026.01.16
[Airflow] interview questions  (0) 2025.12.14
[SQL] interview questions  (0) 2025.12.12

+ Recent posts