뷰·머티리얼라이즈드 뷰 — 쿼리 이름 짓기
자주 쓰는 쿼리에 이름을. 결과를 캐시할지 매번 계산할지의 결정.
같은 복잡한 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 — 한 줄 차이
| VIEW | MATERIALIZED 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 의 진짜 의미.
이전: 12편 jsonb · 현재: 13편 (SQL 기초 마지막) · 다음 → 14편 트랜잭션 (중급 시작) · 진행: 13/24