지난 편에서는 BigQuery의 compute(Dremel)와 storage(Colossus)가 분리된 구조를 봤습니다. 이 분리가 비용에도 그대로 적용됩니다. 쿼리를 돌리면 compute 비용이 발생하고, 데이터를 그냥 저장만 해도 storage 비용이 따로 발생합니다.
이번 편에서는 그중 storage 비용을 다룹니다. 단순히 "데이터 양 × 단가"가 아니라, 어떤 모델로 과금할지를 데이터셋마다 골라야 하고, 그 선택에 따라 같은 1TB 데이터의 비용이 두 배 이상 갈리기도 합니다. 데이터셋을 직접 만드는 입장이 아니더라도, 분석가의 ad-hoc UPDATE 한 번이나 자동화 스크립트가 무심코 돌리는 ALTER가 청구서를 흔드는 경우가 많아서, 시스템을 만지는 모두가 알아야 하는 영역입니다.
1. Logical과 Physical
1-1. 두 가지 과금 단위
BigQuery 데이터셋은 두 가지 storage 과금 모델 중 하나를 선택합니다.
- Logical bytes: 압축되지 않은 원본 크기 기준으로 과금 (기본값)
- Physical bytes: BigQuery가 실제로 디스크(Colossus)에 쓴 압축된 크기 기준으로 과금
원본 데이터 (logical): 1 TiB
압축 후 (physical): ~250 GiB (예시 - 데이터에 따라 다름)
Logical 과금: 1024 GiB × $0.02/GiB-월 = $20.48/월
Physical 과금: 250 GiB × $0.04/GiB-월 = $10.00/월physical 단가가 logical의 두 배지만, 청구되는 바이트 수가 훨씬 적습니다. 절반 이하로만 압축되면 physical이 무조건 유리할 것 같지만 그렇지 않습니다. physical 모드에서는 Time Travel과 Fail-safe도 별도 과금되기 때문에(섹션 3에서 자세히), 압축비만 보고 결정하기는 어렵습니다. 데이터셋별 실제 비용은 4-2 섹션의 쿼리로 직접 측정해서 비교하는 게 정확합니다. 일반적으로는 변경이 적고 압축이 잘 되는 정적 데이터는 physical로 가서 절약되고, 변경이 잦은 데이터는 logical이 안전합니다.
위 단가는 us multi-region 기준입니다. asia-northeast3(서울) 같은 한국 리전은 약 25% 비싸고, 정확한 값은 GCP 공식 가격표에 나와 있습니다. 참고로 logical 모델에는 매월 첫 10 GiB가 무료로 제공되므로, 작은 데이터셋 비교 시에는 이 무료 구간을 감안해야 합니다.
1-2. 데이터셋 단위로 설정
이 모델 선택은 데이터셋 생성 시 옵션으로 정합니다.
-- physical 모델로 데이터셋 생성
CREATE SCHEMA `project.my_dataset`
OPTIONS (
storage_billing_model = 'PHYSICAL',
default_table_expiration_ms = 7776000000, -- 90일 후 자동 삭제 (임시 데이터셋용)
default_partition_expiration_ms = 2592000000 -- 30일 지난 파티션 자동 만료
);
-- 기존 데이터셋의 모델 변경
ALTER SCHEMA `project.my_dataset`
SET OPTIONS (storage_billing_model = 'PHYSICAL');테이블 단위가 아니라 데이터셋 단위입니다. 같은 데이터셋의 모든 테이블이 같은 모델로 묶입니다. 데이터 성격이 다르다면 데이터셋을 나눠야 한다는 뜻입니다.
1-3. 14일 동안 못 바꾼다
한 번 모델을 바꾸면 적용까지 최대 24시간이 걸리고, 그 후 14일 동안 다시 못 바꿉니다. 잘못 골랐다 싶어도 보름은 기다려야 하니, 처음 정할 때 신중해야 합니다.
2. Active와 Long-term
2-1. 90일 기준 자동 분류
데이터셋 안의 테이블(또는 파티션)이 90일 동안 수정되지 않으면, BigQuery가 자동으로 long-term storage로 분류하고 가격을 절반으로 떨어뜨립니다.
테이블이 마지막으로 수정된 시점
│
│← 0일 ────── 90일 ──→
│ active storage │ long-term storage
│ $0.02/GiB-월 (logical) │ $0.01/GiB-월
│ $0.04/GiB-월 (physical) │ $0.02/GiB-월별도 설정이 필요 없고, 가만히 둬도 자동으로 적용됩니다. 1년 전 로그처럼 거의 건드리지 않는 데이터는 신경 쓸 필요 없이 절반 가격이 됩니다.
2-2. 파티션 단위 적용
테이블이 파티셔닝되어 있으면, 파티션 하나하나에 90일 카운터가 따로 붙습니다. 새 파티션이 매일 추가되는 일별 파티션 테이블에서, 90일 이전 파티션들은 자동으로 long-term이 되고 최근 파티션만 active로 남습니다.
이벤트 테이블 (일별 파티션)
2025-01-01 partition → long-term (오래됨)
2025-01-02 partition → long-term
...
2026-02-09 partition → active (90일 이내)
2026-02-10 partition → active
2026-05-10 partition → active (오늘)시계열 테이블처럼 데이터가 시간이 지나면 거의 안 읽히는 구조에서 효과가 큽니다. 같은 테이블 안에서도 오래된 파티션은 자동으로 깎입니다.
2-3. 수정이 무엇인가
다음 작업이 발생하면 active 90일 카운터가 리셋됩니다.
- INSERT, UPDATE, DELETE, MERGE (영향받는 파티션 단위로 적용)
- 테이블 데이터 적재 (load, Storage Write API)
- 클러스터링 키 변경 등 데이터 재기록을 동반하는 ALTER
- 테이블 복사 시 destination 테이블 (source 테이블의 카운터는 영향 없음)
다음은 수정에 포함되지 않습니다.
- SELECT 쿼리
- Snapshot 생성 (source 카운터에 영향 없음)
- Time Travel 조회
- 컬럼 description, label 같은 metadata-only ALTER
- 테이블 클론(CLONE) — 생성 시점에는 zero-copy라 추가 storage 없음. 이후 base 또는 clone이 변경되면 변경분만큼 과금됨
행 단위 UPDATE 한 번이 파티션 전체를 active로 되돌립니다. 일별 파티션 365개짜리 테이블에서, 1년 전 파티션의 단 한 행만 update해도 그 파티션 전체의 90일 카운터가 리셋됩니다. CDC로 한 행씩 업데이트가 들어오는 테이블에서는 long-term으로 떨어지는 파티션 자체가 거의 안 생깁니다. ad-hoc 분석에서도 무심한 UPDATE 한 번이 같은 결과를 만드니, 가능하면 MERGE로 변경분만 반영하거나 새 테이블로 분기하는 게 안전합니다.
Late-arriving data가 오래된 파티션을 active로 되돌립니다. Mixpanel이나 광고 플랫폼, IoT 데이터처럼 며칠씩 늦게 도착하는 이벤트가 흔한 환경에서는, event-time 파티셔닝을 쓰면 1년 전 이벤트가 1년 전 파티션에 적재되어 그 파티션 카운터가 리셋됩니다. 이런 패턴이 잦은 테이블이라면 event-time 파티셔닝이 long-term 분류를 방해하고 있지는 않은지 한 번 점검해 볼 만합니다. 이벤트 발생 시점 기준 분석이 꼭 필요한 게 아니라면 ingestion-time 파티셔닝으로 바꾸는 것만으로 오래된 파티션이 자연스럽게 long-term으로 떨어집니다.
`CREATE OR REPLACE TABLE`을 매일 돌리면 long-term이 안 됩니다. dbt나 cron으로 매일 전체 테이블을 갈아엎는 패턴이 흔한데, 매일 카운터가 리셋되어 90일을 채울 수가 없습니다. 누적 적재가 가능하면 MERGE나 incremental 패턴으로 바꾸는 게 비용에 큰 차이를 냅니다.
2-4. Streaming buffer는 별도 분류
Storage Write API나 legacy streaming insert로 적재한 데이터는 즉시 영구 storage에 쓰이는 게 아니라, streaming buffer라는 별도 영역에 일정 시간(최대 ~90분) 머뭅니다.
비용 분석 관점에서 가장 중요한 점은 buffer 동안의 데이터가 `INFORMATION_SCHEMA.TABLE_STORAGE`에 즉시 반영되지 않는다는 것입니다. 4-2 섹션의 비용 비교 쿼리 결과와 실제 GCP 청구서 사이에 잠깐의 갭이 생기는 원인이 여기에 있습니다. buffer에 있는 동안은 long-term으로 분류되지도 않습니다.
작은 단위로 자주 적재하는 백엔드 코드는 buffer가 항상 차 있는 상태가 되기 쉬워서 비용 추적이 더 어려워집니다. batch load나 Storage Write API의 commit 단위를 적절히 묶는 적재 패턴은 7편에서 다룹니다.
3. Time Travel과 Fail-safe
3-1. 두 단계의 보존 기간
데이터를 삭제하거나 덮어써도, BigQuery는 일정 기간 이전 상태를 보관합니다. 두 단계로 나뉩니다.
지금 ────→ Time Travel ─→ Fail-safe ─→ 영구 삭제
(7일 기본) (7일 고정)
사용자 직접 조회 Google Support 통해서만- Time Travel: 사용자가 SQL로 과거 시점을 직접 조회하거나 삭제된 테이블을 복원할 수 있는 기간
- Fail-safe: Time Travel 끝난 뒤 추가 7일. 사용자는 접근 불가. Google Cloud Support에서만 복구 가능 (재해용 안전망)
-- Time Travel: 1시간 전 시점의 테이블 조회
SELECT *
FROM `project.dataset.events`
FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR);
-- 어제 실수로 지운 테이블 복원
CREATE TABLE `project.dataset.events_restored` AS
SELECT *
FROM `project.dataset.events`
FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY);3-2. Time Travel 기간 조정
Time Travel 기본값은 7일이고, 데이터셋 단위로 줄일 수 있습니다.
ALTER SCHEMA `project.dataset`
SET OPTIONS (max_time_travel_hours = 48); -- 2일로 단축설정 가능한 범위는 2일~7일이고, 24의 배수(48, 72, 96, 120, 144, 168시간)만 입력할 수 있습니다. 짧게 잡으면 storage 비용을 줄일 수 있지만, 실수로 지운 데이터를 되돌릴 수 있는 시간도 짧아집니다.
3-3. 비용 모델에 따라 다른 과금
storage 모델 선택이 청구서에 가장 직접적으로 드러나는 지점이 여기입니다.
Logical 모드:
- Time Travel과 Fail-safe storage가 base storage 가격에 포함
- 청구서에 별도 항목 없음
Physical 모드:
- Time Travel과 Fail-safe가 active rate로 별도 과금
- 데이터를 자주 변경하면 이 두 항목이 청구서에서 큰 자리를 차지
예시: physical 모드 데이터셋 (CDC로 자주 update되는 테이블)
현재 데이터: 100 GiB (active rate로 과금)
Time Travel storage: 30 GiB (지난 7일간 변경된 데이터의 이전 버전)
Fail-safe storage: 25 GiB (Time Travel 만료 후 7일치)
청구: (100 + 30 + 25) GiB × $0.04/GiB-월 = $6.20/월
Time Travel과 Fail-safe storage는 base 데이터가 long-term이어도
항상 active rate로 과금됩니다.logical 모드라면 100 GB만 청구되었을 부분이 physical에서는 변경 이력까지 다 청구됩니다. update가 많은 테이블이라면 이 차이가 매우 커집니다.
`DROP TABLE`로 지운 테이블도 즉시 청구가 멈추지 않습니다. Time Travel(최대 7일) + Fail-safe(7일) 동안은 데이터가 보관되고 storage 비용도 계속 청구됩니다. "어제 1TB 테이블 지웠는데 왜 청구서가 안 줄어요?"라는 질문이 자주 나오는 이유입니다. 즉시 비용을 끊고 싶다면 데이터셋의 max_time_travel_hours를 미리 짧게 줄여 두는 정도가 사용자가 할 수 있는 조정의 전부입니다.
4. 어느 모델을 골라야 하는가
4-1. 세 가지 변수
판단 기준은 세 가지입니다.
압축률
- 압축이 잘 되는 데이터(반복 많은 로그, 카디널리티 낮은 컬럼 다수): physical이 유리
- 카디널리티 높고 무작위에 가까운 데이터: 차이 크지 않음
변경 빈도
- 거의 변경 없는 데이터(정적 dimension, 과거 이벤트 로그): physical이 유리. Time Travel storage가 거의 안 쌓임
- 자주 update/upsert되는 데이터(CDC 테이블, mutation 많은 테이블): logical이 유리. physical이면 변경 이력이 별도로 다 과금됨
보존 정책
- Time Travel을 짧게 잡을 수 있으면 physical 부담이 줄어듦
- 7일 풀로 보존해야 하면 physical에서는 비용 부담이 커짐
대략적인 판단 흐름 (4-2 쿼리 결과 기준):
physical 비용 < logical 비용 (절감폭 큼) → physical
physical 비용 ≈ logical 비용 (차이 미미) → logical (단순함)
TT 비중 높음 + 변경 잦음 → logical (TT 폭증 위험)4-2. 실제 비용 비교
INFORMATION_SCHEMA에 두 모델 모두의 예상 비용을 비교할 수 있는 뷰가 있습니다. 결정 전에 한 번은 돌려보는 게 안전합니다.
여기서 가장 중요한 두 지표는 압축비(logical / physical)와 Time Travel 비중(time_travel / total_physical)입니다. 압축비가 높을수록 physical로 가서 절약될 여지가 크고, Time Travel 비중이 높으면 physical에서 그만큼 추가 과금이 발생한다는 신호입니다.
-- 데이터셋별 logical vs physical 비용 비교 (단가는 us multi-region 기준)
-- 서울 리전이면 `region-asia-northeast3`로 변경, 단가도 GCP 가격표에서 region별로 확인
SELECT
table_schema AS dataset_name,
-- 의사결정 핵심 지표
ROUND(SAFE_DIVIDE(SUM(total_logical_bytes), SUM(total_physical_bytes)), 2) AS compression_ratio,
ROUND(SAFE_DIVIDE(SUM(time_travel_physical_bytes), SUM(total_physical_bytes)) * 100, 2) AS tt_ratio_pct,
-- logical 모드로 청구되는 월 비용
ROUND(
SUM(active_logical_bytes) / POW(1024, 3) * 0.02 +
SUM(long_term_logical_bytes) / POW(1024, 3) * 0.01
, 2) AS logical_cost_usd,
-- physical 모드로 전환 시 월 비용 (TT/FS도 active rate로 합산)
ROUND(
SUM(active_physical_bytes) / POW(1024, 3) * 0.04 +
SUM(long_term_physical_bytes) / POW(1024, 3) * 0.02 +
SUM(time_travel_physical_bytes) / POW(1024, 3) * 0.04 +
SUM(fail_safe_physical_bytes) / POW(1024, 3) * 0.04
, 2) AS physical_cost_usd
FROM `region-us`.INFORMATION_SCHEMA.TABLE_STORAGE_BY_PROJECT
WHERE total_physical_bytes > 0
GROUP BY table_schema
ORDER BY (logical_cost_usd - physical_cost_usd) DESC; -- physical 전환 시 절감 큰 순TABLE_STORAGE_BY_PROJECT는 프로젝트 안의 모든 데이터셋을 보여주고, TABLE_STORAGE는 권한이 있는 데이터셋만 보여줍니다. organization 단위로 보고 싶다면 TABLE_STORAGE_BY_ORGANIZATION 뷰도 있습니다. 단위는 POW(1024, 3)으로 GiB 변환했는데, GCP 공식 가격이 GB가 아니라 GiB(1024³ 바이트)를 기준으로 매겨지기 때문입니다.
현재 billing model을 같이 확인하려면 INFORMATION_SCHEMA.SCHEMATA_OPTIONS에서 option_name = 'storage_billing_model'을 조인하면 됩니다. 이미 physical로 전환된 데이터셋은 logical 비용 컬럼이 가상 시뮬레이션 값이라는 점만 유의하면 됩니다.
추세를 보고 싶다면 이 쿼리를 일별로 별도 history 테이블에 적재해 두는 패턴이 유용합니다. 청구서가 갑자기 튀는 날의 원인을 과거 스냅샷과 비교해서 추적할 수 있습니다. 14일 락이 있어서 한 번 정한 모델은 한동안 유지된다는 점도 함께 따져 보면 좋습니다.
마무리
storage 모델 선택은 결국 데이터의 생애주기에 맞는 옷을 골라 입히는 일에 가깝습니다. 자주 갈아엎는 데이터에 physical을 입히면 변경 이력이 별도 청구서가 되고, 가만히 묵히는 데이터에 logical을 두면 long-term 할인을 누리지 못합니다. 데이터셋마다 변경 빈도와 압축 특성이 다른데, 모든 데이터셋에 같은 모델을 일괄 적용하는 게 가장 흔한 비용 누수 원인입니다.
다음 편에서는 쿼리를 돌릴 때 발생하는 비용을 다룹니다. 1편에서 잠깐 언급했던 슬롯이 실제로 어떻게 동작하고, on-demand와 editions가 어떤 점에서 갈리는지 짚어보겠습니다.