빠른 비교
| 표면 | 조회 시점 | 저장 여부 | 주 용도 |
|---|---|---|---|
VIEW | 조회할 때 원본 쿼리 실행 | 결과 저장 안 함 | 복잡한 쿼리 표면화 |
MATERIALIZED VIEW | refresh 시점 결과 저장 | 결과 저장 | 비싼 집계 캐시 |
기본 구조
VIEW는 저장된 SELECT 정의다
VIEW는 결과를 저장하지 않고 SELECT 정의를 저장합니다. view를 조회하면 PostgreSQL이 원본 테이블에 대한 쿼리처럼 풀어서 실행합니다. 복잡한 JOIN, 필터, 컬럼 alias를 하나의 안정적인 조회 표면으로 감출 때 유용합니다.
CREATE VIEW active_users AS
SELECT id, email, created_at
FROM users
WHERE deleted_at IS NULL;view는 보안 경계처럼 보일 수 있지만, 권한과 RLS, security_barrier 같은 설정을 함께 이해해야 합니다. 단순히 컬럼을 숨기기 위해 view를 만들었다고 해서 모든 우회 경로가 자동으로 막히지는 않습니다.
materialized view는 결과를 저장한다
materialized view는 SELECT 결과를 물리적으로 저장합니다. 조회는 빠를 수 있지만 원본 테이블이 바뀌어도 결과가 자동으로 갱신되지 않습니다. 최신 값이 필요하면 REFRESH MATERIALIZED VIEW를 실행해야 합니다.
CREATE MATERIALIZED VIEW daily_sales AS
SELECT date_trunc('day', created_at) AS day, sum(total) AS revenue
FROM orders
GROUP BY 1;
REFRESH MATERIALIZED VIEW daily_sales;대시보드 집계, 외부 데이터 snapshot, 비싼 join 결과 캐시처럼 "조금 늦어도 되는 읽기"에 맞습니다. 항상 최신이어야 하는 거래성 조회에는 맞지 않습니다.
concurrent refresh는 unique index가 필요하다
REFRESH MATERIALIZED VIEW는 기본적으로 refresh 동안 읽기를 막을 수 있습니다. CONCURRENTLY 옵션을 쓰면 조회 가능성을 높일 수 있지만, materialized view에 적합한 unique index가 필요합니다.
CREATE UNIQUE INDEX daily_sales_day_idx ON daily_sales (day);
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales;concurrent refresh는 일반 refresh보다 더 많은 작업을 할 수 있습니다. 데이터량, 갱신 주기, 조회 지연 허용 범위를 같이 보고 선택해야 합니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 복잡한 SELECT를 재사용 | VIEW |
| 권한을 좁힌 조회 표면 제공 | VIEW + 권한/RLS 검토 |
| 비싼 집계를 캐시 | MATERIALIZED VIEW |
| 항상 최신 결과 필요 | 원본 쿼리 또는 일반 table 설계 |
| refresh 중 조회 유지 | REFRESH MATERIALIZED VIEW CONCURRENTLY |
주의할 점
materialized view는 원본 데이터가 바뀌어도 자동으로 최신화되지 않습니다. refresh 주기와 지연 허용 범위를 정하지 않고 쓰면 대시보드나 리포트가 오래된 값을 보여 줄 수 있습니다. 반대로 일반 view는 결과를 저장하지 않으므로 성능 캐시로 기대하면 안 됩니다.
view를 너무 많이 겹쳐 쌓으면 실제 실행 쿼리를 이해하기 어려워집니다. 성능 문제가 생기면 view 정의를 펼친 실제 SQL과 실행 계획을 확인해야 합니다.
참고 링크
3 sources