874 words
4 minutes
[DE Design Pattern]08-06. Dataset Materializer

6. Read Performance Optimization — Dataset Materializer#

01. Pattern Overvew#

비싼 쿼리의 결과를 물리적으로 저장해두고, 이후 소비자는 저장된 결과만 읽게 하는 패턴

02. Materialized View vs Table 선택#

Materialized View: refresh를 데이터베이스가 관리. 자동 refresh 설정 가능하지만 즉시 반영이 보장되지 않음. BigQuery, Redshift, Databricks, Snowflake 지원.

Table: refresh를 개발자가 직접 관리. 파티셔닝, 버켓팅, 정렬 등 모든 스토리지 최적화 적용 가능. 유연하지만 운영 부담 증가.

  • refresh 타이밍을 정밀하게 제어해야 하거나 스토리지 최적화가 필요하면 Table, 그렇지 않으면 Materialized View

Incremental Refresh의 핵심#

  • 전체 refresh는 데이터가 커질수록 비용이 급증.
  • Incremental refresh는 마지막 실행 이후 변경된 데이터만 반영하여 비용을 줄일 수 있음

이게 가능하려면 두 가지 조건이 필요하다:

  1. 변경 시점을 식별할 수 있는 컬럼이 존재해야 함 (예: insertion_time)
  2. 집계 로직이 incremental하게 합산 가능해야 함 (예: count는 더하면 되지만, median은 불가)

BigQuery 자동 Refresh Materialized View#

# BigQuery에서 15분마다 자동 refresh되는 materialized view
# (Python BigQuery 클라이언트 기준)
from google.cloud import bigquery
client = bigquery.Client()
sql = """
CREATE MATERIALIZED VIEW dedp.visits.visits_enriched
OPTIONS (enable_refresh = true, refresh_interval_minutes = 15)
AS SELECT
v.visit_id,
v.event_time,
v.page,
u.login,
d.type AS device_type
FROM dedp.visits.raw_visits v
LEFT JOIN dedp.visits.users u ON v.user_id = u.id
LEFT JOIN dedp.visits.devices d ON v.device_id = d.id
"""
client.query(sql).result()

PostgreSQL 수동 Refresh#

-- 1) Materialized View 생성
CREATE MATERIALIZED VIEW dedp.windowed_visits AS
SELECT
v.visit_id, v.event_time, v.page,
u.login, t.browser
FROM dedp.visits_events v
LEFT JOIN dedp.user_context u ON v.visit_id = u.visit_id
LEFT JOIN dedp.tech_context t ON v.visit_id = t.visit_id
WHERE v.event_time >= CURRENT_DATE - INTERVAL '21 days';
-- 2) 수동 refresh (CONCURRENTLY: refresh 중에도 읽기 가능)
REFRESH MATERIALIZED VIEW dedp.windowed_visits WITH CONCURRENTLY;

WITH CONCURRENTLY가 없으면 refresh 중에 해당 view를 읽을 수 없다.

  • 운영 환경에서는 거의 항상 CONCURRENTLY를 사용
  • unique index가 있어야 이 옵션이 동작

MERGE 기반 Incremental Refresh#

-- 마지막 실행 이후 추가된 데이터만 반영
MERGE INTO dedp.visits_counter AS target
USING (
SELECT user_id, COUNT(*) AS visits
FROM dedp.visits_all
WHERE insertion_time > '2024-11-09T03:27:32'
GROUP BY user_id
) AS input
ON target.user_id = input.user_id
WHEN MATCHED THEN
UPDATE SET count = count + input.visits
WHEN NOT MATCHED THEN
INSERT (user_id, count) VALUES (input.user_id, input.visits);

03. Consequences#

Refresh cost: 전체 refresh는 비싼 연산. Incremental refresh로 완화하되, 모든 SQL 연산이 incremental을 지원하지는 않는다.

Data access 제어: materialized view가 원본 테이블의 접근 제어를 우회할 수 있다. 원본에 접근 권한이 없는 사용자가 materialized 결과를 통해 데이터를 볼 수 있는 보안 이슈.

Storage overhead: 데이터를 복제하므로 저장 비용 증가. 일부만 materialize하는 혼합 전략(자주 쓰는 쿼리만 materialize)으로 완화.


Concept

  • Dataset Materializer : 비용이 큰 쿼리 결과를 물리적으로 저장하여 반복 읽기 성능을 최적화하는 패턴
  • Materialized View : 데이터베이스가 refresh를 관리하는 구현. 자동/수동 refresh 지원
  • Table 기반 Materialization : 개발자가 refresh를 직접 관리. 모든 스토리지 최적화 적용 가능
  • Incremental Refresh : 마지막 실행 이후 변경된 데이터만 반영하는 refresh 방식. 전체 refresh보다 가벼움
  • MERGE (Upsert) : 있으면 UPDATE, 없으면 INSERT를 한 문장으로 처리하는 SQL 연산
  • CONCURRENTLY : PostgreSQL에서 materialized view refresh 중에도 읽기를 허용하는 옵션
  • Auto Refresh : BigQuery, Redshift 등에서 materialized view를 자동으로 갱신하는 기능. 즉시 반영은 보장되지 않음

[DE Design Pattern]08-06. Dataset Materializer
https://yjinheon.netlify.app/posts/02de/00-de-design-pattern/08_data_storage/08-06_dataset_materializer/
Author
Datamind
Published at
2026-03-27
License
CC BY-NC-SA 4.0