구체화된 뷰(Materialized View)는 쿼리 결과를 물리적으로 저장하는 특수 뷰다. 일반 뷰와 달리 실제 데이터를 디스크에 저장하여 빠른 읽기를 제공하지만, 데이터 최신성을 위해 주기적으로 갱신해야 한다. 복잡한 집계, 리포트, OLAP 쿼리에 유용하다.
sql
-- 일별 매출 집계를 구체화된 뷰로 생성
CREATE MATERIALIZED VIEW daily_sales AS
SELECT
DATE(created_at) AS sale_date,
category,
COUNT(*) AS order_count,
SUM(total) AS revenue,
AVG(total) AS avg_order_value
FROM orders
JOIN products ON orders.product_id = products.id
GROUP BY DATE(created_at), category
WITH DATA; -- 즉시 데이터 채우기
-- 인덱스 생성 (일반 뷰는 불가, 구체화된 뷰는 가능)
CREATE INDEX ON daily_sales (sale_date);
CREATE INDEX ON daily_sales (category, sale_date);
-- 데이터 조회 (매우 빠름)
SELECT * FROM daily_sales WHERE sale_date >= '2024-01-01';
갱신 방법
sql
-- 전체 갱신 (기존 데이터 삭제 후 재생성)
REFRESH MATERIALIZED VIEW daily_sales;
-- 동시 갱신 (조회 차단 없음, UNIQUE 인덱스 필요)
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales;
-- 자동 갱신 (pg_cron 사용)
SELECT cron.schedule('refresh-daily-sales', '0 2 * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales');
성능 비교
복잡한 집계 쿼리 (10억 건 주문 테이블):
일반 뷰: ~120초 (매번 전체 집계)
구체화된 뷰: ~0.01초 (저장된 결과 반환)
갱신 비용: ~60초 (새벽 배치)
점진적 갱신 패턴
PostgreSQL의 기본 REFRESH는 전체 재생성이다. 점진적 갱신을 구현하려면:
sql
-- 마지막 갱신 이후 변경분만 처리
CREATE FUNCTION refresh_daily_sales_incremental()
RETURNS void AS $$
DECLARE
last_refresh TIMESTAMP;
BEGIN
SELECT last_refresh INTO last_refresh FROM mv_metadata WHERE mv_name = 'daily_sales';
-- 변경된 날짜만 삭제 후 재삽입
DELETE FROM daily_sales WHERE sale_date >= last_refresh::DATE;
INSERT INTO daily_sales SELECT ... FROM orders WHERE created_at >= last_refresh;
UPDATE mv_metadata SET last_refresh = NOW() WHERE mv_name = 'daily_sales';
END;
$$ LANGUAGE plpgsql;
관련 개념