PostgreSQL 교재 · 13편 / 24편

뷰·머티리얼라이즈드 뷰 — 쿼리 이름 짓기

자주 쓰는 쿼리에 이름을. 결과를 캐시할지 매번 계산할지의 결정.

SQL 기초읽는 시간 6분2026-05-17
VIEW 와 MATERIALIZED VIEW 의 차이를 비교하는 다이어그램

같은 복잡한 SELECT 를 여러 곳에서 쓰고 있다면 — 로 만들 시기입니다. SQL 의 함수 같은 도구. 결과를 매번 계산하는 VIEW 와 캐시하는 MATERIALIZED VIEW 의 차이, REFRESH 시점, 표현식 인덱스까지 — SQL 기초 파트의 마지막입니다.

VIEW — 쿼리에 이름 붙이기

-- 자주 쓰는 "활성 사용자" 쿼리
CREATE VIEW active_users AS
SELECT id, name, email, created_at
FROM   users
WHERE  active = true
  AND  deleted_at IS NULL;

-- 사용
SELECT * FROM active_users WHERE created_at >= '2026-01-01';
SELECT count(*) FROM active_users;

-- 정의 변경 (재생성)
CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, last_login_at AS created_at
FROM   users
WHERE  active = true;

-- 삭제
DROP VIEW active_users;

뷰의 가치 한 줄. 복잡한 비즈니스 규칙(예: "활성 사용자란 무엇인가") 을 한 곳에 정의하고, 애플리케이션 코드에서는 깔끔한 이름만 씁니다. 규칙이 바뀌면 뷰 정의만 수정.

VIEW vs MATERIALIZED VIEW — 한 줄 차이

VIEWMATERIALIZED VIEW
실행 시점매 조회REFRESH 때만
결과 저장없음 (가상)디스크에 실제 저장
속도원본 쿼리와 같음매우 빠름 (캐시)
최신성실시간REFRESH 시점 기준
인덱스원본 테이블만자체에 인덱스 가능
INSERT/UPDATE제한적 (단순 뷰만)불가

MATERIALIZED VIEW — 비싼 집계 캐시

-- 일별 매출 집계 (1000만 행에서 GROUP BY — 매번 5초 걸린다 가정)
CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', created_at)::date AS day,
       COUNT(*)                            AS orders,
       SUM(total)                          AS revenue
FROM   orders
GROUP BY date_trunc('day', created_at)::date;

-- 자체 인덱스 (조회 가속)
CREATE UNIQUE INDEX ON daily_revenue (day);

-- 조회 — 빠름 (캐시된 결과)
SELECT * FROM daily_revenue WHERE day >= CURRENT_DATE - 30;

-- 새로고침
REFRESH MATERIALIZED VIEW daily_revenue;

-- 잠금 없이 갱신 (UNIQUE 인덱스 필수)
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;

REFRESH 의 비용. 일반 REFRESH 는 전체를 다시 계산합니다(점진 갱신 X). 매시간 같은 주기로 cron 으로 돌리는 게 보통. CONCURRENTLY 옵션을 쓰면 갱신 중에도 조회 가능 — 단 UNIQUE 인덱스가 반드시 있어야 함.

언제 MV, 언제 일반 VIEW

  • VIEW — 매번 빠른 쿼리, 실시간 정확성 필수, 권한·보안 추상화.
  • MATERIALIZED VIEW — 비싼 집계, 약간 옛 데이터 OK, 대시보드/분석.
  • 둘 다 부적합한 경우 — 대용량 + 실시간 + 자주 변경. 이 셋이 다 필요하면 별도 테이블 + 트리거(19편) 또는 점진 갱신 도구(pg_ivm·timescaledb continuous aggregate).

표현식 인덱스 — VIEW 가 아니어도 빠르게

-- 자주 "이메일 소문자 비교" 한다면
CREATE INDEX ix_users_email_lower ON users ((lower(email)));

-- 쿼리에서도 표현식 그대로 써야 인덱스가 효력
SELECT * FROM users WHERE lower(email) = lower($1);

-- 다른 예 — date_trunc
CREATE INDEX ix_orders_day ON orders (date_trunc('day', created_at));
SELECT count(*) FROM orders WHERE date_trunc('day', created_at) = '2026-05-17';

표현식 인덱스는 "쿼리에서 자주 계산하는 식 자체에 인덱스 거는 것". 일반 컬럼 인덱스로는 안 잡히는 패턴을 가속해 줍니다.

실전 패턴 — 권한·보안에 뷰 활용

-- 사용자에게는 비밀번호 컬럼 가리기
CREATE VIEW user_public AS
SELECT id, name, email, created_at
FROM   users;

-- 권한
REVOKE SELECT ON users FROM app_role;          -- 원본 막고
GRANT SELECT ON user_public TO app_role;       -- 뷰만 허용

-- WITH CHECK OPTION — 뷰 조건 위반 INSERT/UPDATE 차단
CREATE VIEW active_users AS
SELECT * FROM users WHERE active = true
WITH CHECK OPTION;

INSERT INTO active_users (name, active) VALUES ('A', false);
-- ❌ ERROR: new row violates check option for view "active_users"

일반적 안티패턴

1. 뷰 위에 뷰 위에 뷰. 5단계 중첩되면 옵티마이저가 못 따라가고, 진단도 지옥. 1-2 단계까지.

2. MV REFRESH 를 트리거로. 행 1개 바뀔 때마다 전체 REFRESH 면 시스템이 죽음. cron 또는 ETL 파이프라인에서 일정 주기.

3. 뷰로 권한 우회. SECURITY DEFINER 뷰는 권한 escalation 위험. 신중하게 — 보통 GRANT 와 RLS(행 단위 보안) 가 더 안전.

14편 — 트랜잭션 (중급 시작)

BEGIN·COMMIT·ROLLBACK 과 격리 수준 4종. ACID 의 진짜 의미.

📚 PostgreSQL 배우기 교재
이전: 12편 jsonb · 현재: 13편 (SQL 기초 마지막) · 다음 → 14편 트랜잭션 (중급 시작) · 진행: 13/24

© 2026 주나이테크(주) @JUNAITECH