LLM WikiAccess-protected knowledge portal
← 스터디 홈
7편 · 약 22분

성능 최적화: partition, clustering, materialization, aggregate table

분석 모델 성능은 쿼리 튜닝만으로 끝나지 않는다

분석 데이터 모델이 느릴 때 가장 먼저 WHERE 조건, 조인 순서, 인덱스, 실행계획을 본다. 하지만 데이터 웨어하우스와 lakehouse에서는 쿼리 한 줄보다 모델이 어떻게 저장되고, 얼마나 미리 계산되어 있으며, 어떤 단위로 읽히는가가 더 큰 차이를 만든다.

예를 들어 매일 수억 건의 이벤트에서 일자 × 국가 × 채널별 매출을 대시보드가 계속 읽는다면, 원천 이벤트를 매번 스캔해서 집계하는 방식은 비싸다. 이때 성능 최적화의 질문은 다음처럼 바뀐다.

  • 쿼리가 최근 7일만 읽는다면, 테이블도 날짜 단위로 잘라져 있는가?
  • 같은 날짜 안에서 customer_id, country, event_name 같은 필터가 반복된다면, 저장 블록이 그 컬럼 기준으로 모여 있는가?
  • BI가 매번 같은 집계를 계산한다면, 집계 결과를 미리 저장할 수 있는가?
  • 모델이 너무 느리게 빌드된다면, 매번 전체를 다시 만들 필요가 있는가?

이 장에서는 분석 모델 성능 최적화를 partition, clustering, materialization, aggregate table 네 축으로 정리한다. 특정 엔진 하나의 문법 암기가 아니라, 운영자가 어떤 상황에서 어떤 비용을 줄이는지 판단하는 것이 목표다.


1. 네 가지 최적화 레버를 먼저 구분하기

Partition 읽을 날짜·범위만 파티션 pruning Clustering 같은 값끼리 모아 블록 pruning Materialization view/table/incremental 저장 방식 선택 Aggregate table 반복 집계를 미리 계산 목표: 스캔량, 셔플, 반복 계산, 빌드 시간을 줄인다 단, 저장 비용·refresh 비용·운영 복잡도·데이터 신선도 지연을 함께 지불한다 운영 판단 빠른 쿼리만 보지 말고, 누가 얼마나 자주 읽고 얼마나 자주 바뀌는지부터 확인한다
분석 모델 성능 최적화의 네 가지 레버

네 레버는 서로 대체 관계가 아니라 조합 관계다. 큰 이벤트 fact table은 날짜로 partition하고, 자주 필터링되는 customer_idevent_name으로 clustering할 수 있다. 그 위에 일자 × 채널 집계 테이블을 만들고, dbt에서는 해당 모델을 incremental로 빌드해 매일 새 파티션만 처리할 수 있다.

운영자가 피해야 할 실수는 모든 모델에 모든 최적화를 넣는 것이다. 파티션은 메타데이터와 관리 비용이 있고, clustering은 유지 비용이 있으며, materialized view나 aggregate table은 저장 공간과 refresh 책임을 만든다.


2. Partition: 읽을 범위를 물리적으로 줄인다

Partition은 테이블을 날짜, 시간, 정수 범위 같은 기준으로 나누는 방식이다. BigQuery 문서의 표현을 빌리면, 쿼리가 partitioning column에 조건을 걸 때 엔진은 해당 조건과 맞는 파티션만 스캔하고 나머지를 건너뛸 수 있다. 이 과정을 partition pruning이라고 부른다.

분석 모델에서 가장 흔한 partition key는 다음과 같다.

모델 유형흔한 partition key이유
이벤트 factevent_date, event_timestamp대부분 최근 N일 조회, backfill도 날짜 단위
주문 factorder_date, created_date매출·주문 대시보드가 기간 필터를 기본으로 사용
스냅샷 테이블snapshot_date특정 기준일 상태를 조회
로그·감사 테이블ingested_at, log_date보존 정책과 삭제가 날짜 단위

2.1 Partition이 효과적인 조건

Partition은 다음 조건을 만족할 때 효과가 크다.

  • 대부분의 쿼리가 partition key에 필터를 건다.
  • 한 파티션이 너무 작지 않다.
  • 보존, 삭제, 재처리, backfill이 partition 단위로 일어난다.
  • 날짜 범위가 명확해서 dry run이나 사전 비용 추정이 중요하다.

BigQuery는 partition이 너무 잘게 쪼개져 작은 파티션이 많아지면 메타데이터 오버헤드가 커질 수 있다고 설명한다. 그래서 “무조건 일 단위”가 정답은 아니다. 데이터 양이 작고 조회 단위가 월별이면 월 partition이 더 단순하다. 반대로 초대형 로그에서 시간 단위 재처리가 필요하면 hourly partition도 검토할 수 있다.

2.2 dbt BigQuery 예시

{{ config(
    materialized = 'incremental',
    partition_by = {
      "field": "event_date",
      "data_type": "date",
      "granularity": "day"
    },
    require_partition_filter = true,
    cluster_by = ["customer_id", "event_name"]
) }}

select
  event_date,
  customer_id,
  event_name,
  count(*) as event_count
from {{ ref('stg_events') }}
where event_date >= date_sub(current_date(), interval 30 day)
group by 1, 2, 3

require_partition_filter = true는 큰 테이블을 실수로 전체 스캔하지 않도록 막는 안전장치다. 다만 이 설정은 다른 dbt 모델이나 테스트가 해당 모델을 읽을 때도 partition filter를 요구하므로, 운영 적용 전 downstream SQL을 확인해야 한다.


3. Clustering: 같은 값이 가까이 저장되도록 만든다

Partition이 “큰 구획을 건너뛰는” 방식이라면, clustering은 파티션 안이나 전체 테이블 안에서 데이터를 특정 컬럼 순서로 모아 저장해 storage block pruning을 돕는 방식이다.

BigQuery clustered table은 clustered column 기준의 사용자 정의 정렬 순서를 갖고, 필터나 집계가 clustering column을 사용할 때 관련 storage block만 읽을 수 있다. Snowflake도 micro-partition pruning 관점에서 비슷한 효과를 기대한다. 다만 Snowflake는 clustering key 유지에 credit 비용이 들 수 있으므로 매우 큰 테이블, 자주 읽히고 상대적으로 덜 바뀌는 테이블에 신중히 적용하라고 안내한다.

3.1 Clustering key 선택 기준

좋은 clustering key는 다음 특징을 갖는다.

  1. 쿼리 필터에 자주 등장한다. 예: customer_id, country, event_name, merchant_id.
  2. 선택도가 있다. 필터를 걸었을 때 전체의 작은 일부만 읽는다.
  3. 동일한 키 조합이 많은 쿼리에서 반복된다. 일부 쿼리만 좋아지고 대부분은 효과가 없다면 유지 비용이 아깝다.
  4. 컬럼 순서가 실제 필터 패턴과 맞다. BigQuery는 clustering column 순서가 중요하다. 예를 들어 (event_date, country, customer_id)로 clustering했는데 쿼리가 customer_id만 단독 필터링하면 기대 효과가 제한적일 수 있다.

3.2 Partition과 Clustering의 조합

가장 흔한 조합은 “날짜 partition + 사용자/상품/지역 clustering”이다.

fact_events
  partition: event_date
  cluster: customer_id, event_name

쿼리:
  where event_date between '2026-07-01' and '2026-07-07'
    and customer_id = 12345

이 경우 엔진은 먼저 날짜 파티션을 줄이고, 각 파티션 안에서 customer_id 관련 블록만 읽으려 한다. 즉 partition은 시간 범위 절단, clustering은 범위 안의 세부 절단에 가깝다.


4. Materialization: 계산을 언제 저장할지 결정한다

dbt 기준으로 materialization은 모델을 warehouse에 어떤 형태로 남길지 정하는 전략이다. dbt 기본 내장 materialization에는 view, table, incremental, ephemeral, materialized_view가 있다.

materialization장점비용·주의점적합한 모델
view저장 공간 거의 없음, 항상 최신 원천 반영복잡한 view stack은 매번 계산되어 느릴 수 있음가벼운 staging, rename, type cast
table읽기 빠름, BI에 안정적매번 재빌드하면 빌드 시간이 길 수 있음많이 읽히는 mart, 느린 변환 결과
incremental새 데이터만 처리해 빌드 시간·비용 절감unique key, late arriving data, full refresh 정책 필요이벤트, append 중심 대용량 fact
ephemeralDB 객체를 만들지 않고 CTE로 삽입과용하면 SQL이 길어지고 디버깅 어려움아주 가벼운 중간 로직
materialized_viewDB가 refresh를 관리하는 사전 계산 객체엔진별 제약·비용·지원 범위 차이반복 조회되는 비싼 집계·필터

중요한 원칙은 처음부터 모두 table이나 incremental로 만들지 않는 것이다. dbt 문서도 일반적으로 view에서 시작하고, 성능 문제가 실제로 관측될 때 다른 materialization으로 바꾸는 접근을 권장한다.

4.1 Incremental 모델의 운영 포인트

Incremental은 “지난 실행 이후 바뀐 데이터만 처리한다”는 아이디어다. dbt의 is_incremental()은 대상 테이블이 이미 존재하고, --full-refresh가 아니며, 모델이 incremental로 설정된 경우에만 참이 된다.

{{ config(
    materialized = 'incremental',
    unique_key = 'order_id'
) }}

select
  order_id,
  customer_id,
  order_status,
  updated_at
from {{ ref('stg_orders') }}

{% if is_incremental() %}
where updated_at >= (
  select coalesce(max(updated_at), timestamp '1900-01-01') from {{ this }}
)
{% endif %}

운영에서 확인할 점은 세 가지다.

  • updated_at이 신뢰 가능한가? 지연 도착 데이터가 cutoff 이전으로 들어오면 누락될 수 있다.
  • unique_key가 실제로 유일한가? 중복 key는 merge 결과를 불안정하게 만든다.
  • full refresh는 언제, 얼마나 오래 걸리는가? schema 변경이나 로직 수정 때 전체 재생성이 필요할 수 있다.

5. Aggregate table: 반복 집계를 미리 계산한다

Aggregate table은 원천 fact보다 높은 grain으로 미리 집계한 테이블이다. 예를 들어 fact_orders가 주문 1건 단위라면, agg_daily_revenue_by_channeldate × channel 단위로 매출을 미리 계산한다.

create table mart.agg_daily_revenue_by_channel as
select
  order_date,
  channel,
  count(*) as order_count,
  sum(order_amount) as revenue
from mart.fact_orders
group by 1, 2;

BI 대시보드가 늘 일자별 매출만 본다면 원천 주문 테이블을 매번 읽을 이유가 없다. aggregate table은 scan량, group by 비용, BI 응답 시간을 줄인다.

5.1 Aggregate table을 만들 기준

다음 조건이 겹칠수록 aggregate table 후보가 된다.

  • 같은 group by가 여러 대시보드와 리포트에서 반복된다.
  • 원천 fact가 크고 집계 결과는 작다.
  • 사용자가 보는 지표 grain이 안정적이다.
  • 약간의 refresh 지연을 허용할 수 있다.
  • 지표 정의 owner가 명확해서 숫자 변경을 통제할 수 있다.

반대로 ad hoc 탐색이 많고 group by 조합이 자주 바뀌면 aggregate table을 많이 만들수록 관리 비용이 커진다. 이때는 metric layer나 semantic model에서 공통 정의를 관리하고, 실제 물리화는 사용량이 검증된 지표부터 적용하는 편이 낫다.

5.2 Materialized view와 aggregate table의 차이

두 방식 모두 미리 계산한다는 점은 비슷하지만 운영 책임이 다르다.

구분Aggregate tableMaterialized view
refresh 주체Airflow/dbt/job 등 사용자가 설계DB 엔진이 일부 또는 전체 관리
제어력높음. backfill, 보정, 품질 검증을 세밀하게 설계엔진 제약 안에서 동작
신선도스케줄에 따름엔진별 자동 refresh 또는 최신성 보장 방식
비용빌드 job 비용 + 저장 비용저장 비용 + 유지 compute 비용
적합명확한 배치 운영, 검증·승인 절차 필요반복 쿼리 패턴이 안정적이고 엔진 지원이 충분

Snowflake 문서는 materialized view가 반복되는 비싼 query pattern, 특히 집계·필터·projection·semi-structured 처리에 유용하지만 저장과 유지 compute 비용이 있다고 설명한다. PostgreSQL materialized view는 저장된 query 결과를 table처럼 읽고, REFRESH MATERIALIZED VIEW로 갱신한다. ClickHouse incremental materialized view는 insert 시점에 결과를 target table로 보내는 trigger에 가까워, 쿼리 시점 비용을 insert 시점으로 옮기는 성격이 강하다.


6. 성능 최적화는 측정 없이 적용하지 않는다

분석 모델 성능 개선은 “느릴 것 같아서”가 아니라 실제 관측으로 시작해야 한다.

6.1 적용 전 확인할 지표

확인 항목봐야 하는 것
쿼리 빈도BI, scheduled query, notebook, downstream model에서 얼마나 자주 읽는가
스캔량bytes scanned, partitions scanned, micro-partitions scanned
실행 시간p50/p95 latency, queue time과 execution time 분리
비용query cost, warehouse credit, slot 사용량
빌드 시간dbt run duration, incremental merge duration, full refresh duration
신선도dashboard SLA, source freshness, refresh delay

6.2 변경 후 검증 방법

최적화 적용 후에는 최소한 다음을 비교한다.

Before
- 대표 쿼리 5~10개 실행 시간
- 스캔 bytes / partition 수
- warehouse 비용 또는 slot 사용량
- dbt 모델 빌드 시간

After
- 같은 쿼리, 같은 기간, 같은 warehouse 크기로 재측정
- explain / query profile에서 partition pruning 또는 block pruning 확인
- 숫자 동일성 검증: row count, sum, key별 reconciliation
- refresh 지연과 실패 시 복구 절차 확인

성능이 빨라졌더라도 숫자가 달라졌거나 refresh 실패 시 복구가 어렵다면 운영적으로 성공한 최적화가 아니다.


7. 운영 체크리스트

성능 최적화를 PR로 올릴 때는 다음 질문에 답해야 한다.

□ 이 모델의 주요 소비자는 누구인가? BI, downstream dbt, ad hoc user 중 무엇인가?
□ 대표 쿼리와 조회 기간은 무엇인가?
□ partition key가 실제 WHERE 조건에 항상 포함되는가?
□ clustering key 순서는 실제 필터 패턴과 맞는가?
□ materialization 변경으로 저장 비용과 build 시간이 어떻게 바뀌는가?
□ incremental cutoff가 지연 도착 데이터와 update 이벤트를 놓치지 않는가?
□ aggregate table의 grain과 metric definition owner가 명확한가?
□ full refresh, backfill, rollback 절차가 문서화되어 있는가?
□ 변경 전후 숫자 reconciliation 결과가 있는가?

이 체크리스트의 핵심은 성능을 “개별 쿼리의 속도”가 아니라 모델 운영 계약으로 보는 것이다. 빠르게 읽히는 모델은 좋지만, 신선도·정확성·비용·복구 가능성이 함께 설명되어야 한다.


8. 실무 판단 예시

8.1 이벤트 fact table이 너무 비싼 경우

  • 증상: 최근 30일 유입 분석 대시보드가 매번 전체 이벤트 테이블을 스캔한다.
  • 적용: event_date partition, user_id 또는 event_name clustering, BI 쿼리에 날짜 필터 강제.
  • 검증: 날짜 필터 없는 쿼리가 실패하거나 리뷰에서 차단되는지 확인한다.

8.2 dbt mart 빌드가 매일 2시간 걸리는 경우

  • 증상: 전체 주문 fact를 매일 재집계한다.
  • 적용: incremental materialization, updated_at cutoff, 최근 N일 재처리 window.
  • 검증: 늦게 도착한 주문과 상태 변경 주문이 누락되지 않는지 reconciliation한다.

8.3 임원 대시보드가 월요일 아침마다 느린 경우

  • 증상: 같은 주간 KPI 집계를 여러 dashboard tile이 반복 계산한다.
  • 적용: agg_weekly_kpi_by_product 같은 aggregate table 또는 warehouse materialized view.
  • 검증: 원천 fact와 집계 테이블의 금액·건수 합계가 일치하는지, refresh SLA가 dashboard SLA보다 빠른지 확인한다.

마무리

분석 모델 성능 최적화는 SQL을 멋지게 바꾸는 일이 아니라, 데이터가 읽히는 경로를 짧게 만드는 일이다. Partition은 읽을 날짜와 범위를 줄이고, clustering은 범위 안에서 필요한 블록만 읽게 도와준다. Materialization은 계산을 매번 할지 저장할지 정하고, aggregate table은 반복 집계를 사용자 응답 시간 밖으로 밀어낸다.

다만 모든 최적화는 비용을 만든다. 저장 공간, refresh job, clustering maintenance, full refresh, backfill, 숫자 검증이 뒤따른다. 그래서 좋은 운영자는 “이 모델이 느리다”에서 바로 설정을 추가하지 않는다. 먼저 어떤 쿼리가, 얼마나 자주, 어떤 grain으로, 어떤 SLA 아래 읽히는지 확인하고, 그 소비 패턴에 맞는 물리 설계를 선택한다.

References

  • Google Cloud BigQuery Documentation, “Introduction to partitioned tables”, https://cloud.google.com/bigquery/docs/partitioned-tables
  • Google Cloud BigQuery Documentation, “Introduction to clustered tables”, https://cloud.google.com/bigquery/docs/clustered-tables
  • dbt Developer Hub, “Materializations”, https://docs.getdbt.com/docs/build/materializations
  • dbt Developer Hub, “Configure incremental models”, https://docs.getdbt.com/docs/build/incremental-models
  • dbt Developer Hub, “BigQuery configurations”, https://docs.getdbt.com/reference/resource-configs/bigquery-configs
  • Snowflake Documentation, “Clustering Keys & Clustered Tables”, https://docs.snowflake.com/en/user-guide/tables-clustering-keys
  • Snowflake Documentation, “Working with Materialized Views”, https://docs.snowflake.com/en/user-guide/views-materialized
  • PostgreSQL Documentation, “Materialized Views”, https://www.postgresql.org/docs/current/rules-materializedviews.html
  • ClickHouse Documentation, “Incremental materialized views”, https://clickhouse.com/docs/materialized-view/incremental-materialized-view