쿼리에 이름을 붙여 테이블처럼 쓰게 하는 객체. 데이터를 저장하지 않는다.
일반 뷰 — 저장하지 않는다
CREATE VIEW active_member AS
SELECT id, name, email FROM member WHERE deleted_at IS NULL;
SELECT * FROM active_member WHERE email = 'a@b.com';
-- 실행 시 뷰 정의가 쿼리에 합쳐진다(인라인). 별도 저장 공간이 없다
용도 세 가지
- ① 복잡한 쿼리를 이름으로 감춘다 (가독성·재사용)
- ② 권한 통제 — 민감 칼럼을 뺀 뷰만 조회 권한을 준다
- ③ 스키마 변경을 흡수한다 — 테이블을 바꿔도 뷰 정의만 고치면 사용처가 안 바뀐다
권한 통제 예시
CREATE VIEW member_public AS SELECT id, name FROM member; -- 주민번호·전화 제외
REVOKE ALL ON member FROM analyst;
GRANT SELECT ON member_public TO analyst;
구체화 뷰 — 결과를 실제로 저장한다
CREATE MATERIALIZED VIEW daily_sales AS
SELECT date(created_at) AS d, sum(amount) AS total FROM orders GROUP BY 1;
CREATE UNIQUE INDEX ON daily_sales (d); -- CONCURRENTLY 갱신에 필수
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales;
- 일반 뷰 — 항상 최신. 매번 계산한다 → 무거운 집계면 느리다
- 구체화 뷰 — 미리 계산해 저장한다 → 조회가 빠르다. 대신 갱신 전까지 낡은다
CONCURRENTLY 없이 갱신하면 그동안 조회가 막힌다 — 운영에서 자주 데는 지점이고, 쓰려면 유니크 인덱스가 미리 있어야 한다.
MySQL에는 구체화 뷰가 없어 요약 테이블 + 배치로 직접 만든다.
갱신 가능한 뷰
-- 단순한 뷰는 INSERT/UPDATE 가 원본 테이블로 전달된다
UPDATE active_member SET name = '홍길동' WHERE id = 1; -- member 가 갱신된다
-- 조인·집계·DISTINCT 가 들어가면 불가능하다 → INSTEAD OF 트리거로 직접 구현
성능상의 함정
-- 뷰를 뷰 위에 여러 겹 쌓으면 옵티마이저가 감당하지 못한다
CREATE VIEW v3 AS SELECT * FROM v2 JOIN ...; -- v2 는 v1 위에, v1 은 또 조인 위에
-- → 계획이 폭발하거나 불필요한 테이블까지 읽는다
뷰는 저장 프로시저가 아니라 쿼리 텍스트의 치환에 가깝다. 겹치면 그만큼 큰 쿼리가 된다.
면접 함정
- ❌ "뷰를 쓰면 빨라진다" → 일반 뷰는 성능과 무관하다. 매번 계산한다.
- ❌ "뷰는 읽기 전용" → 단순 뷰는 갱신 가능하다.
뷰가 성능을 망치는 실제 사례
-- 뷰 안에 이미 집계가 있는데 바깥에서 한 건만 찾는다
CREATE VIEW v_member_stat AS
SELECT member_id, count(*) cnt, sum(amount) total FROM orders GROUP BY member_id;
SELECT * FROM v_member_stat WHERE member_id = 42;
-- 옵티마이저가 조건을 안으로 밀어 넣지 못하면
-- → 전체 회원을 집계한 뒤 하나만 골라낸다
EXPLAIN ANALYZE SELECT * FROM v_member_stat WHERE member_id = 42;
-- GroupAggregate ... rows=1000000 ← 조건이 안 밀려 들어갔다는 신호
조건이 밀려 들어가는지(predicate pushdown) 계획으로 확인하는 것이 뷰를 쓸 때의 필수 점검이다.
뷰 정의를 바꿀 때
CREATE OR REPLACE VIEW v AS SELECT ...; -- 칼럼 개수·이름·타입이 같아야 한다
DROP VIEW v; CREATE VIEW v AS ...; -- 구조가 바뀌면 이렇게. 의존 객체가 깨진다
-- 이 뷰에 무엇이 의존하고 있나
SELECT dependent_ns.nspname, dependent_view.relname
FROM pg_depend d JOIN pg_rewrite r ON r.oid = d.objid
JOIN pg_class dependent_view ON dependent_view.oid = r.ev_class
JOIN pg_namespace dependent_ns ON dependent_ns.oid = dependent_view.relnamespace
WHERE d.refobjid = 'v'::regclass;