컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 16

뷰 / 구체화 뷰

SQL구체화뷰뷰

뷰(View)는 SELECT 문에 이름을 붙여 저장해 두고 테이블처럼 조회하는 객체입니다. 일반 뷰는 쿼리 정의만 저장하므로 조회할 때마다 그 쿼리를 원본 테이블에 실행하고, 항상 현재 원본의 데이터를 보여 줍니다.

구체화 뷰(Materialized View)는 쿼리를 한 번 실행해 그 결과를 실제 데이터로 저장해 두는 객체입니다. 조회는 저장된 결과를 읽으므로 가볍지만, 원본이 바뀌어도 자동으로 반영되지 않아 REFRESH로 갱신해야 합니다. 이 토픽에서는 일반 뷰와 구체화 뷰의 차이, 저장된 결과가 오래되는 과정, REFRESH 방식을 시각화로 확인합니다.

핵심 개념

일반 뷰

이름을 붙여 저장한 쿼리입니다. 데이터는 저장하지 않고 조회할 때마다 원본에 쿼리가 실행됩니다.

핵심 개념

구체화 뷰

쿼리 결과를 실제 데이터로 저장해 둔 뷰입니다. 조회는 저장된 결과를 읽으므로 무거운 쿼리도 가볍게 조회합니다.

핵심 개념

REFRESH

구체화 뷰의 저장된 결과를 원본 기준으로 다시 만드는 명령입니다. PostgreSQL에서는 REFRESH MATERIALIZED VIEW를 직접 실행해야 갱신됩니다.

핵심 개념

신선도

구체화 뷰는 갱신하기 전까지 원본보다 오래된 데이터를 보여 줄 수 있습니다. 허용할 수 있는 지연을 정해 갱신 주기를 잡습니다.

핵심 개념

CONCURRENTLY

PostgreSQL에서 갱신하는 동안 조회를 막지 않는 REFRESH 옵션입니다. 표현식과 WHERE 없이 컬럼으로만 만든 UNIQUE 인덱스가 필요합니다.

핵심 개념

추상화와 보안

뷰로 복잡한 쿼리를 감싸 단순하게 쓰고, 보여 줄 열을 제한할 수 있습니다. 행 제한을 믿고 쓰려면 PostgreSQL에서는 security_barrier를 함께 써야 합니다.

일반 뷰는 항상 최신입니다 — 조회할 때 원본에 쿼리를 실행하므로 원본의 변경이 다음 조회에 바로 보입니다. 대신 무거운 쿼리는 조회마다 계산 비용이 듭니다.
구체화 뷰는 조회가 빠르고 오래될 수 있습니다 — 저장된 결과를 읽을 뿐이라 가볍지만, REFRESH 전까지는 원본과 차이가 날 수 있습니다.
저장 공간을 씁니다 — 구체화 뷰는 결과를 실제 데이터로 저장하므로 저장 공간이 들고, 일반 뷰는 쿼리 정의만 저장합니다.
갱신 방식에 따라 조회가 막힐 수 있습니다 — PostgreSQL의 옵션 없는 REFRESH는 갱신 동안 조회를 막을 수 있고, CONCURRENTLY는 막지 않는 대신 컬럼만으로 만든 UNIQUE 인덱스가 필요하고 이미 채워진 구체화 뷰에만 쓸 수 있습니다.
이 시각화에서 확인할 것 — "일반 뷰" 탭에서 조회마다 원본 행을 읽어 최신 합계를 돌려주는 모습을, "구체화 뷰" 탭에서 저장된 결과를 읽다가 원본에 행이 늘어도 오래된 값이 나오고 REFRESH로 바뀌는 모습을, "REFRESH" 탭에서 일반 갱신 동안 조회가 기다리고 CONCURRENTLY에서는 조회가 이어지는 모습을 STEP으로 확인하세요.

뷰와 구체화 뷰의 배경

뷰는 SQL 표준에 있는 기능으로, 복잡한 쿼리를 이름 하나로 재사용하고 사용자에게 보이는 데이터를 제한하는 데 쓰입니다. PostgreSQL 튜토리얼은 뷰를 폭넓게 쓰는 것을 좋은 SQL 데이터베이스 설계의 핵심 요소 중 하나로 설명합니다.

구체화 뷰는 무거운 집계나 조인 결과를 미리 계산해 두는 용도로 쓰입니다. PostgreSQL은 9.3 버전부터 구체화 뷰를 지원하고 Oracle에도 있으며, 문법과 동작은 제품마다 다릅니다. MySQL 서버는 구체화 뷰를 제공하지 않아 요약 테이블을 직접 갱신하는 방식을 쓰기도 합니다.

숫자로 확인하기

원본 6행, 결과 3행 — orders 6행을 지역별로 합산하면 KR 350, US 450, JP 80의 3행이 됩니다.
행 추가 뒤 조회 — KR 150 행을 추가한 뒤 이 장난감 모델에서 일반 뷰는 원본 7행을 읽어 KR 500을 돌려주고, 구체화 뷰는 저장된 3행을 읽어 KR 350을 돌려줍니다.
REFRESH 뒤 — 쿼리가 원본 7행을 다시 읽어 KR 합계가 500으로 바뀌고, 코드 모델의 읽은 행 누적은 뷰 조회 7행과 REFRESH 7행을 합쳐 14행입니다.
코드 확인 — 파이썬과 자바 모델 모두 뷰 500(읽은 행 7), 구체화 뷰 350(읽은 행 7), REFRESH 뒤 500(읽은 행 14)을 출력합니다.

자주 하는 오해

일반 뷰는 결과를 저장해 빠르다 — 일반 뷰는 쿼리만 저장하고 조회마다 실행하므로, 원본 쿼리보다 빨라지지 않습니다. 빠르게 하려면 구체화 뷰나 인덱스가 필요합니다.
구체화 뷰는 원본과 자동으로 동기화된다 — PostgreSQL에서는 REFRESH를 실행해야 갱신됩니다. 데이터베이스마다 자동 갱신 지원이 다릅니다.
구체화 뷰가 항상 일반 뷰보다 빠르다 — 저장된 결과를 읽는 조회는 가볍지만, 갱신할 때 쿼리를 다시 실행하는 비용이 들고 자주 갱신하면 이점이 줄어듭니다.
뷰는 데이터를 복사해 둔 것이다 — 일반 뷰는 데이터를 복사하지 않습니다. 복사해 저장하는 것은 구체화 뷰입니다.
구체화 뷰는 테이블처럼 수정할 수 있다 — PostgreSQL의 구체화 뷰는 INSERT나 UPDATE로 직접 수정할 수 없고 REFRESH로만 내용이 바뀝니다.
뷰로 접근을 제한하면 완전히 안전하다 — 뷰는 보여 줄 열과 행을 줄이는 수단이지만, 권한 설정과 함께 써야 하며 쿼리 조건을 통해 정보가 새지 않도록 보안 옵션을 확인해야 합니다.

면접에서 이렇게 물어요

면접 질문

일반 뷰의 특징은?

쿼리 정의만 저장하고 조회할 때마다 원본 테이블에 실행합니다. 항상 최신 데이터를 보여 주지만 복잡한 쿼리는 매번 계산 비용이 듭니다.

면접 질문

구체화 뷰의 특징은?

쿼리 결과를 실제로 저장해 두고 REFRESH로 갱신합니다. 조회가 빠르지만 원본이 바뀌어도 자동으로 반영되지 않습니다.

면접 질문

구체화 뷰를 쓸 때 주의할 점은?

갱신 전까지 오래된 데이터를 볼 수 있으므로 허용할 지연을 정해 갱신 주기를 잡아야 합니다. 저장 공간과 갱신 비용도 고려합니다.

면접 질문

REFRESH CONCURRENTLY는 무엇이 다른가요?

갱신하는 동안 조회를 막지 않습니다. 대신 컬럼으로만 만든 UNIQUE 인덱스가 필요하고 변경이 많으면 일반 REFRESH보다 오래 걸릴 수 있습니다.

활용 분야

복잡한 쿼리의 재사용 — 여러 테이블을 조인하는 쿼리에 이름을 붙여 애플리케이션과 분석 쿼리가 같은 정의를 쓰게 합니다.
집계 리포트와 대시보드 — 일별, 지역별 합계처럼 무거운 집계를 구체화 뷰로 미리 계산해 두고 정해진 주기로 갱신합니다.
접근 제어 — 민감한 열을 뺀 뷰를 만들어 일부 사용자에게만 뷰에 대한 권한을 줍니다.

함께 보면 좋은 토픽

인덱스 — 구체화 뷰에도 인덱스를 만들어 조회를 더 빠르게 합니다.
쿼리 실행 계획 — 뷰를 조회할 때 실제로 어떻게 실행되는지 확인합니다.
SQL 조인 — 뷰에 자주 담기는 조인 쿼리의 종류를 봅니다.
락 — REFRESH가 잡는 잠금과 조회의 관계를 이어서 봅니다.

참고 자료

SQL — PostgreSQL의 뷰와 구체화 뷰

CREATE VIEW region_sales AS
SELECT region, SUM(amount) AS total FROM orders GROUP BY region;
SELECT * FROM region_sales;                           -- 조회할 때마다 위 쿼리가 원본에 실행된다

CREATE MATERIALIZED VIEW region_mv AS
SELECT region, SUM(amount) AS total FROM orders GROUP BY region
WITH DATA;                                          -- 만들 때 결과를 채운다 (기본값)

CREATE UNIQUE INDEX ON region_mv (region);               -- CONCURRENTLY 갱신에 필요

REFRESH MATERIALIZED VIEW region_mv;                    -- 갱신 동안 조회가 막힐 수 있다
REFRESH MATERIALIZED VIEW CONCURRENTLY region_mv;      -- 조회를 막지 않고 갱신

SELECT matviewname, ispopulated FROM pg_matviews;     -- 구체화 뷰 목록과 채워졌는지 여부

일반 뷰는 CREATE VIEW로 쿼리에 이름을 붙여 저장하고, 조회할 때마다 그 쿼리를 원본 테이블에 실행합니다. 구체화 뷰는 CREATE MATERIALIZED VIEW가 쿼리를 한 번 실행해 결과를 실제 데이터로 저장하며, 원본이 바뀌어도 자동으로 반영되지 않고 REFRESH로 갱신해야 합니다. 시각화의 일반 뷰와 구체화 뷰 탭이 보여 주는 차이와 같습니다. REFRESH MATERIALIZED VIEW는 옵션 없이 쓰면 갱신하는 동안 조회를 막을 수 있고, CONCURRENTLY를 붙이면 조회를 막지 않고 갱신하지만 구체화 뷰에 컬럼만으로 만든(표현식과 WHERE가 없는) UNIQUE 인덱스가 있어야 하고 이미 채워진 상태여야 합니다. 시각화의 REFRESH 탭이 보여 주는 동작입니다. 자세한 문법과 제약은 PostgreSQL 문서를 확인하세요.

Python — 조회마다 계산하는 뷰와 저장된 구체화 뷰

base = [('KR', 100), ('KR', 200), ('US', 150), ('KR', 50), ('US', 300), ('JP', 80)]
reads = 0

def query():                                  # 지역별 합계 쿼리
    global reads
    reads += len(base)                        # 원본에서 읽은 행 수
    out = {}
    for region, amount in base:
        out[region] = out.get(region, 0) + amount
    return out

def view():                                   # 일반 뷰: 조회할 때마다 쿼리 실행
    return query()

stored = query()                              # 구체화 뷰: 만들 때 한 번 실행해 저장

def mview():
    return stored                             # 조회는 저장된 결과를 읽음

def refresh():
    global stored
    stored = query()                          # REFRESH: 쿼리를 다시 실행해 저장

reads = 0
base.append(('KR', 150))                      # 원본에 새 행
print('뷰', view()['KR'], '읽은 행', reads)
print('구체화 뷰', mview()['KR'], '읽은 행', reads)
refresh()
print('REFRESH 뒤', mview()['KR'], '읽은 행', reads)
# 뷰 500 읽은 행 7
# 구체화 뷰 350 읽은 행 7
# REFRESH 뒤 500 읽은 행 14

두 방식의 차이를 읽은 행 수로 보여 주는 장난감 모델입니다. 원본에 KR 150 행을 추가한 뒤 뷰를 조회하면 쿼리가 원본 7행을 읽어 최신 합계 500을 돌려줍니다. 구체화 뷰는 저장된 결과를 읽을 뿐이라 읽은 행이 늘지 않고, 값은 갱신 전의 350 그대로입니다. REFRESH가 쿼리를 다시 실행해 원본 7행을 더 읽으면 총 14행이 되고 값이 500으로 바뀝니다. 시각화의 구체화 뷰 탭과 같은 흐름입니다. 구체화 뷰의 조회가 가볍다는 것은 저장된 결과를 읽기만 하기 때문이고, 그 대가가 갱신 전까지 오래된 값을 읽는 것입니다.

Java — 같은 모델을 자바로

static List<Map.Entry<String, Integer>> base = new ArrayList<>(List.of(
    Map.entry("KR", 100), Map.entry("KR", 200), Map.entry("US", 150),
    Map.entry("KR", 50), Map.entry("US", 300), Map.entry("JP", 80)));
static int reads = 0;
static Map<String, Integer> stored;

static Map<String, Integer> query() {
    reads += base.size();
    Map<String, Integer> out = new TreeMap<>();
    for (Map.Entry<String, Integer> e : base) out.merge(e.getKey(), e.getValue(), Integer::sum);
    return out;
}

stored = query();                                  // 구체화 뷰를 만들 때 한 번 실행
reads = 0;
base.add(Map.entry("KR", 150));
System.out.println("뷰 " + query().get("KR") + " 읽은 행 " + reads);      // 뷰 500 읽은 행 7
System.out.println("구체화 뷰 " + stored.get("KR") + " 읽은 행 " + reads);  // 구체화 뷰 350 읽은 행 7
stored = query();                                  // REFRESH
System.out.println("REFRESH 뒤 " + stored.get("KR") + " 읽은 행 " + reads);  // REFRESH 뒤 500 읽은 행 14

파이썬 모델과 같은 흐름을 자바로 옮긴 코드로 출력도 같습니다. 아래 조각은 main 메서드 안에서 실행한다고 가정했습니다. query가 조회마다 원본 행을 모두 읽어 합계를 구하고, 구체화 뷰에 해당하는 stored는 만들 때 한 번 채워 둔 결과를 그대로 돌려줍니다. 원본에 행이 늘어난 뒤에도 stored는 REFRESH에 해당하는 query 재실행 전까지 350입니다. 실제 데이터베이스는 이렇게 애플리케이션 코드에서 구체화하지 않고 구체화 뷰를 SQL 객체로 제공합니다. 일반 뷰의 쿼리 재작성, 구체화 뷰의 저장과 갱신은 모두 데이터베이스가 처리합니다. 이 코드는 개념 설명용입니다.

Q1

일반 뷰(View)의 특징은?

뷰는 이름을 붙인 SELECT 문이라고 생각하면 됩니다.
일반 뷰는 쿼리 정의만 저장해 두고, 조회할 때마다 그 쿼리를 원본 테이블에 대해 실행합니다. 항상 최신 데이터를 보여주지만 복잡한 쿼리라면 매번 계산 비용이 듭니다.

Q2

구체화 뷰(Materialized View)의 특징은?

"구체화" — 결과를 물리적으로 만들어 둡니다.
Materialized View는 쿼리 결과를 실제 데이터로 저장하므로 조회가 빠릅니다. 대신 PostgreSQL에서는 원본이 바뀌어도 자동으로 반영되지 않아 REFRESH로 갱신해야 합니다.

Q3

구체화 뷰를 사용할 때 주의할 점은?

결과를 "복사해 둔" 것이라 원본과 시차가 생깁니다.
PostgreSQL의 구체화 뷰는 저장된 결과가 REFRESH 전까지 유지되므로 원본과 차이가 날 수 있습니다. 집계 리포트처럼 약간의 지연이 허용되는 무거운 쿼리에 적합합니다.