본문 바로가기

6. 뷰 3종 - 일반 뷰, 승인된 뷰, 머티리얼라이즈드 뷰

·9분 읽기·
목차

지난 편까지는 데이터를 어떻게 저장하고 어떻게 빠르게 읽을지를 주로 다뤘습니다. 이번 편에서는 같은 데이터를 다른 형태로 노출하는 얘기를 합니다. 뷰라고 부르는 도구가 그 역할을 하는데, BigQuery에서는 성격이 다른 뷰가 셋 있습니다.

이름이 다 "뷰"라서 비슷해 보이지만 하는 일이 꽤 다릅니다. 일반 뷰는 SQL을 저장해 두고 매번 재실행하는 형태입니다. 머티리얼라이즈드 뷰는 결과를 실제로 저장해 두는 대신 스토리지 비용과 지원 함수 제약이 붙습니다. 승인된 뷰는 성격이 아예 다른데, 접근 권한을 통제하는 용도입니다.

1. 뷰가 여러 종류인 이유

1-1. 하나의 데이터, 여러 소비 방식

원본 테이블 하나는 보통 여러 방식으로 소비됩니다. 대시보드에는 일자별 요약이 필요합니다. 분석가는 raw 데이터에 자유롭게 쿼리하고 싶어 하고, 파트너 팀에는 특정 컬럼만 노출해야 합니다. 이걸 매번 raw 테이블 위에 서로 다른 쿼리를 쓰게 하면 관리가 안 됩니다.

뷰는 이 문제를 서로 다른 방향에서 풉니다. 일반 뷰는 쿼리 로직을 재사용하는 목적이라 결과를 저장하지 않고 매번 재실행합니다. MV는 반복 집계의 compute 비용을 줄이려고 결과 자체를 저장해 두고, 조회 시 그 결과를 그대로 가져옵니다. 승인된 뷰는 성격이 아예 다른데, 원본 접근 없이 결과만 노출하려는 접근 제어용이라 일반 뷰 위에 authorize 처리를 얹어 씁니다. 목적이 다르니 하나를 다른 것으로 대체할 수 없습니다.


2. 일반 뷰

2-1. 저장된 쿼리

일반 뷰는 이름 그대로 SQL을 저장해 둔 것입니다. 매번 그 SQL을 재실행합니다.

sql
CREATE VIEW `project.dataset.daily_active_users` AS
SELECT
  event_date,
  COUNT(DISTINCT user_id) AS dau
FROM `project.dataset.events`
WHERE event_type = 'app_open'
GROUP BY event_date;

-- 사용 시
SELECT * FROM `project.dataset.daily_active_users`
WHERE event_date = '2026-07-01';

뷰 자체에는 데이터가 없어서 스토리지 비용이 0입니다. 대신 뷰를 쿼리하면 정의된 SQL이 실행되고, 그 스캔량과 슬롯 사용량이 그대로 청구됩니다.

2-2. 함정: 대시보드에서 반복 호출

일반 뷰의 가장 흔한 함정은 매번 재실행된다는 사실이 잊히는 것입니다. Looker Studio, Tableau 같은 BI 도구가 뷰를 뿌리 삼아 대시보드를 만들면 사용자가 필터를 바꿀 때마다, 자동 새로고침이 돌 때마다 뷰 정의 SQL 전체가 재실행됩니다.

대시보드 자동 새로고침 15분마다
  → 뷰 쿼리 매번 전체 실행
  → 하루 96번 × 스캔 200GB
  → 월 스캔량 약 570TB

같은 뷰가 MV였다면:
  → base 테이블 변경분만 refresh
  → 조회는 저장된 결과에서 → 스캔 미미

BigQuery에는 무료 쿼리 결과 캐시(24시간)가 있고, Looker Studio 같은 BI 도구도 자체 캐시를 한 겹 갖고 있습니다. 그런데 대시보드 환경에서는 이 캐시가 잘 안 붙습니다. 사용자가 필터를 클릭해 WHERE 조건이 바뀌면 쿼리 텍스트가 달라져 캐시 미스가 나고, base 테이블에 새 데이터가 append되면 BigQuery 결과 캐시가 즉시 무효화됩니다. 실시간 스트리밍이 붙어 있으면 캐시가 생겼다 사라지길 반복합니다. 위 570TB는 캐시가 전혀 안 먹었을 때의 상한이지만, 필터 변경이 잦고 base가 계속 append되는 대시보드 워크로드에서는 이 상한에 꽤 가까이 가곤 합니다.

뷰 이름이 daily_summary처럼 무해해 보여도 뒤에 붙은 SQL이 조인 여러 개짜리 무거운 쿼리라면 청구서가 조용히 늘어납니다. 대시보드가 자주 새로고침하는 뷰라면 MV로 옮기는 걸 검토해 볼 만합니다.


3. 머티리얼라이즈드 뷰

3-1. 결과를 저장한다

머티리얼라이즈드 뷰(MV)는 정의된 쿼리의 결과를 실제 테이블처럼 저장합니다. 조회할 때는 저장된 결과를 그대로 가져오니 스캔이 거의 없고 응답이 빠릅니다.

sql
CREATE MATERIALIZED VIEW `project.dataset.mv_daily_users` AS
SELECT
  event_date,
  COUNT(DISTINCT user_id) AS dau,
  COUNT(*) AS total_events
FROM `project.dataset.events`
GROUP BY event_date;

일반 뷰와 다른 점은 결과를 저장하기 때문에 storage 비용이 별도로 발생한다는 것입니다. active/long-term 분류나 physical/logical 모델은 일반 테이블과 동일하게 적용됩니다(2편 참고).

3-2. Incremental refresh

MV의 강점은 base 테이블이 변경돼도 전체를 재계산하지 않고 변경분(델타)만 계산해서 결과를 갱신한다는 점입니다. 로그 테이블처럼 append-only에 가까운 데이터에서 특히 잘 맞습니다.

base_table (events) 오늘 파티션에 100만 행 추가


MV는 오늘 파티션 델타만 재집계


전체 3년치를 다시 안 읽음 → refresh 비용이 매우 작음

Incremental refresh에는 제약이 있습니다. JOIN이 있는 MV라면 left side 테이블에만 append가 가능하고, right side가 변경되면 incremental refresh가 안 됩니다. 그래서 실무에서는 큰 append-only 팩트 테이블을 left에, 상대적으로 변하지 않는 dimension 테이블을 right에 놓는 패턴이 흔합니다.

3-3. Smart tuning

smart tuning은 사용자가 base 테이블을 직접 쿼리해도 BigQuery 옵티마이저가 "이 쿼리는 MV로 처리 가능하네"라고 판단하면 자동으로 MV를 대신 사용해 주는 기능입니다.

sql
-- 사용자 쿼리 (MV 존재 여부 몰라도 됨)
SELECT event_date, COUNT(DISTINCT user_id) AS dau
FROM `project.dataset.events`
WHERE event_date >= '2026-07-01'
GROUP BY event_date;

-- BigQuery가 자동으로 이 쿼리로 재작성
SELECT event_date, dau
FROM `project.dataset.mv_daily_users`
WHERE event_date >= '2026-07-01';

기존 쿼리를 하나도 안 바꿔도 MV의 이득을 봅니다. 대시보드나 노트북에 박혀 있는 SQL을 새 뷰 이름으로 다 바꾸는 마이그레이션이 필요 없다는 뜻입니다.

단, smart tuning은 주로 incremental MV에서 폭넓게 동작합니다. 뒤에서 다룰 allow_non_incremental_definition 옵션을 켠 MV는 rewrite 조건이 더 까다로워서 이득이 잘 안 붙는 경우가 많습니다.

3-4. max_staleness

MV는 base 테이블이 변경될 때마다 자동으로 refresh하는데, 자주 변경되는 테이블에서는 refresh 비용이 부담이 됩니다. 이걸 완화하는 옵션이 max_staleness입니다.

sql
CREATE MATERIALIZED VIEW `project.dataset.mv_daily_users`
OPTIONS (
  max_staleness = INTERVAL 1 HOUR
) AS
SELECT event_date, COUNT(DISTINCT user_id) AS dau
FROM `project.dataset.events`
GROUP BY event_date;
  • 조회 시 결과가 1시간 이내에 refresh됐으면 저장된 결과 그대로 반환
  • 1시간을 넘겼으면 그 시점에 base 테이블 변경분을 반영해 refresh 후 반환

max_staleness는 "조회 결과가 이만큼 오래된 것까지는 허용한다"는 상한을 INTERVAL로 지정하는 옵션이라, refresh 자체의 빈도를 정하는 옵션과는 층위가 다릅니다. refresh 빈도는 refresh_interval_minutes로 따로 잡고, 두 값을 함께 쓰면 "refresh는 이 주기로 시도하되, 조회할 때는 이 정도 오래된 것까지 허용"이라는 조합을 만들 수 있습니다. 예를 들어 refresh_interval_minutes = 30, max_staleness = 1 HOUR로 잡으면, 30분마다 refresh를 시도하되 조회 시점에는 마지막 refresh가 1시간 이내면 캐시로 답하고, 1시간을 넘겼으면 그 시점에 base 델타를 보정해 반환합니다.

대시보드가 "5분 전 데이터"까지는 필요 없고 "1시간 이내면 OK"라면 1 HOUR로 잡아 두는 게 refresh 비용을 크게 아낍니다.

3-5. 지원 함수 제약

Incremental MV에서 못 쓰는 것들이 꽤 있습니다.

- Window function (ROW_NUMBER, RANK, LAG 등)
- Non-deterministic function (RAND, CURRENT_DATE, SESSION_USER, CURRENT_TIME)
- User Defined Function (UDF)
- Array 서브쿼리
- 집계 결과를 다시 필터링 (HAVING의 일부 형태)
- OUTER JOIN 계열의 일부

incremental refresh가 성립하려면 base 델타만 보고도 결과 델타를 정확히 계산할 수 있어야 합니다. 위 함수들은 그게 불가능하기 때문에 지원 목록에서 빠져 있습니다. ROW_NUMBER는 전체 순서를 봐야 하고, RAND()는 매번 값이 다릅니다.

이 제약이 답답할 때 쓰는 옵션이 allow_non_incremental_definition입니다.

sql
CREATE MATERIALIZED VIEW `project.dataset.mv_ranked`
OPTIONS (
  allow_non_incremental_definition = TRUE,
  max_staleness = INTERVAL 6 HOUR
) AS
SELECT
  user_id,
  ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_ts DESC) AS rn
FROM `project.dataset.events`;

이 옵션을 켜면 OUTER JOIN, HAVING, UNION, window function 등을 쓸 수 있습니다. 대신 incremental refresh가 안 되고 매번 전체 재계산이 되며, allow_non_incremental_definition을 켤 때는 `max_staleness`를 반드시 함께 지정해야 합니다(안 하면 create가 거부됨). smart tuning도 incremental MV처럼 광범위하게 붙지는 않아서, 옵티마이저 rewrite 대상이 되는 조건이 더 까다롭습니다. 이런 손실 때문에 non-incremental MV는 정말 필요할 때만 쓰고, 가능하면 쿼리를 재구조화해서 incremental로 만드는 게 낫습니다.


4. 승인된 뷰

4-1. 접근 권한을 뒤집는 도구

일반 뷰나 MV는 성능/재사용 도구인 반면, 승인된 뷰는 접근 제어 도구입니다. 사용자가 뷰는 조회할 수 있지만 그 뷰가 참조하는 원본 테이블에는 권한이 없어도 되는 구조를 만듭니다.

일반 상황:
  user_A → 뷰 조회 시도
  → 뷰가 events 테이블 참조
  → user_A는 events 권한 없음 → 실패

승인된 뷰:
  user_A → 뷰 조회 시도
  → 뷰가 승인된 상태 (events 데이터셋에서 authorize 처리)
  → user_A는 뷰 권한만 있으면 됨
  → 뷰가 events 데이터를 대신 조회 → 성공

권한 흐름이 "사용자 → 뷰 → 원본" 대신 "사용자 → 뷰(권한 승계)"로 짧아지는 셈입니다. 뷰가 원본을 대신 조회하는 대리인 역할을 합니다.

4-2. 실무 시나리오

흔한 사례 하나. raw_events 데이터셋에 개인정보 컬럼 포함한 원본 로그가 있는데 마케팅 팀에는 개인정보를 뺀 집계 결과만 노출해야 한다면 이렇게 씁니다.

sql
-- 1) marketing_views 데이터셋에 뷰 생성 (개인정보 제외 + 집계)
CREATE VIEW `project.marketing_views.daily_users` AS
SELECT
  event_date,
  region,
  COUNT(DISTINCT user_id) AS dau
FROM `project.raw_events.events`
GROUP BY event_date, region;

-- 2) raw_events 데이터셋에서 이 뷰를 authorize
--    (콘솔 또는 API로 dataset access control 설정)

이제 마케팅 팀에는 marketing_views 데이터셋 조회 권한만 주면 됩니다. 그들은 raw_events의 존재를 몰라도 되고 권한도 없습니다.

4-3. 승인된 데이터셋

뷰가 몇 개 안 되면 개별 승인이 관리 가능하지만, 뷰 수십 개짜리 데이터셋이라면 하나씩 승인하기 번거롭습니다. 그럴 때 승인된 데이터셋을 씁니다.

데이터셋 A(raw)에서 데이터셋 B를 "authorized dataset"으로 등록
  → 데이터셋 B에 만들어진 모든 뷰가 자동으로 A 접근 승인

새 뷰를 추가할 때마다 개별 승인이 필요 없어져서 multi-tenant 아키텍처에 특히 유용합니다.

4-4. 승인된 루틴

같은 개념을 SQL 함수나 저장 프로시저에 적용한 것이 승인된 루틴입니다. 파라미터를 받아 결과를 반환하는 로직에 접근 제어를 걸고 싶을 때 씁니다.

sql
-- raw 데이터셋의 events에 접근하는 SQL 함수
CREATE FUNCTION `project.utils.get_user_events`(user_id STRING)
RETURNS ARRAY<STRUCT<event_date DATE, event_type STRING>>
AS (
  ARRAY(
    SELECT AS STRUCT event_date, event_type
    FROM `project.raw_events.events`
    WHERE user_id = get_user_events.user_id
  )
);

-- raw_events 데이터셋에서 이 루틴을 authorize

호출자는 함수만 쓸 수 있고, 함수가 raw 테이블을 대신 조회합니다. 필터 로직을 함수 안에 넣으면 사용자가 파라미터를 조작해도 정해진 범위 밖으로 벗어날 수 없습니다.

4-5. 한도 - 2,500개

한 데이터셋의 access control list에 등록할 수 있는 authorized 리소스(뷰 + 데이터셋 + 함수)는 총 2,500개까지입니다. 뷰가 많은 대규모 multi-tenant 환경에서는 이 한도에 닿을 수 있고, 그럴 때 대응 방법이 뷰 여러 개를 데이터셋 단위로 묶어 authorized dataset으로 등록하는 것입니다. 승인 개수가 뷰 수 대신 데이터셋 수가 되어 부담이 크게 줄어듭니다.


5. 언제 무엇을 쓰는가

뷰마다 목적이 다르니 상황별로 선택 기준이 갈립니다.

쿼리 로직 재사용 + 스캔량이 크지 않음    →  일반 뷰
반복 집계 쿼리로 compute 비용 큼         →  MV
원본 권한 없이 결과만 노출 필요          →  승인된 뷰 (일반 뷰 위에 authorize)
스키마 여러 개에 걸친 뷰 대량 관리        →  승인된 데이터셋
파라미터화된 접근 로직                   →  승인된 루틴

여러 개를 섞어 쓰는 경우도 흔합니다. 원본 데이터셋 위에 MV로 사전 집계하고, 그 MV를 승인된 뷰로 감싸서 외부 팀에 노출하는 식입니다. MV의 성능 이득과 승인된 뷰의 접근 제어가 함께 걸립니다.

한 가지 자주 나오는 실무 질문: "MV 자체를 승인된 뷰로 만들 수 있나요?" 됩니다. MV도 일반 뷰와 같은 방식으로 authorize할 수 있고, 이걸 authorized materialized view라고 부릅니다. 소스 데이터셋의 access control에 MV를 authorize 대상으로 등록하면 됩니다. 일반 뷰로 감싸서 두 단계로 만드는 방법도 여전히 동작하지만, MV를 직접 authorize할 수 있으니 굳이 그럴 필요는 없습니다.


마무리

같은 데이터를 두고도 재사용, 반복 계산 비용, 접근 통제라는 서로 다른 필요가 겹칩니다. 이 필요를 한 종류의 뷰로 다 풀지 못해서 셋으로 갈렸다고 보는 게 편합니다. 특히 MV의 smart tuning은 기존 쿼리를 안 건드리고도 이득이 붙어서, 대시보드 쿼리가 무겁게 도는 워크로드에서는 도입 부담이 낮은 편입니다.

다음 편에서는 데이터를 넣는 쪽을 다룹니다. batch load부터 Storage Write API의 네 가지 스트림(default/committed/pending/buffered)까지, 언제 어느 방식으로 넣어야 뒤 편에서 다룰 CDC나 스트리밍 분석과 매끄럽게 붙는지를 살펴봅니다.