이 글은 BigQuery에서 날짜 조건으로 로그 데이터를 조회할 때 쿼리 비용이 너무 크게 나와 이유를 알고 싶은 클라우드 학습자를 위한 실습 기록입니다.
문제는 날짜 조건을 WHERE절에 넣었는데도 BigQuery가 예상보다 많은 데이터를 스캔한다는 점입니다.
이 글을 통해 날짜 파티션 테이블을 직접 만들고, 왜 파티션이 쿼리 비용과 성능에 중요한지 실습으로 이해할 수 있습니다.
이 글의 핵심 질문
BigQuery에서 날짜 조건으로 조회할 때 왜 날짜 파티션 테이블이 필요하고, 어떻게 직접 만들어야 하는가?
실습 환경
- Cloud: Google Cloud Platform
- 서비스: BigQuery
- Dataset:
ecommerce - 원본 테이블:
data-to-insights.ecommerce.all_sessions_rawbigquery-public-data.noaa_gsod.gsod*
- 실습 생성 테이블:
ecommerce.partition_by_dayecommerce.days_with_rain
- 핵심 주제: 날짜 파티션, 처리 바이트 절감, 자동 만료 파티션
아키텍처
이번 실습의 핵심 구조는 매우 단순하지만 효과는 큽니다. 기존의 비파티션 원본 테이블은 날짜 조건이 있어도 전체 테이블을 읽은 뒤 필터링하는 구조입니다. 반면 날짜 문자열을 PARSE_DATE()로 변환하고 PARTITION BY를 적용하면, BigQuery는 필요한 날짜 파티션만 읽게 됩니다. 즉, 같은 날짜 조회라도 “전체 스캔 후 필터”와 “필요한 파티션만 선택”이라는 완전히 다른 실행 구조가 됩니다.

" alt="BigQuery 날짜 파티션 아키텍처" style="max-width:100%; height:auto; border-radius:12px;">
Raw Table → PARSE_DATE 변환 → Partitioned Table → Full Scan vs Partition Scan 비교
전체 흐름
이번 실습은 크게 5단계로 진행됩니다.
- 실습용 Dataset 생성
- 비파티션 테이블에서 날짜 필터 쿼리 비용 확인
PARSE_DATE()로 날짜 변환 후 파티션 테이블 생성- 파티션 테이블에서 처리량 감소 확인
- 자동 만료 파티션 테이블 생성 및 보존 기간 검증
즉, 이번 글의 핵심은 단순히 테이블을 새로 만드는 것이 아니라 BigQuery가 어떤 방식으로 데이터를 읽는지 구조를 바꾸는 것입니다.
왜 이 실습이 중요한가
BigQuery를 처음 쓰면 이런 생각을 하기 쉽습니다.
- 날짜 WHERE절이 있으면 그 날짜만 읽을 것이다
- LIMIT 5를 넣으면 스캔량도 작아질 것이다
하지만 비파티션 테이블에서는 이 가정이 틀릴 수 있습니다. 실습 문서도 2017년 특정 날짜를 조회하든, 존재하지 않는 2018년 날짜를 조회하든, 비파티션 테이블에서는 여전히 1.74GB를 처리한다고 설명합니다. 엔진은 조건을 만족하는지 확인하기 위해 전체를 읽어야 하기 때문입니다. :contentReference[oaicite:0]{index=0}
즉, 이번 실습은 SQL 문법보다 먼저 BigQuery의 스캔 구조를 이해하는 실습입니다.
■ 강사 설명
실습 문서는 먼저 비파티션 테이블에서 날짜 조건 조회가 얼마나 비효율적인지 보여준 뒤, PARTITION BY를 사용해 날짜 파티션 테이블을 만들도록 안내합니다. 이후 파티션 테이블에서는 필요한 날짜만 읽기 때문에 처리 바이트 수가 급격히 줄어든다는 점을 확인합니다. 마지막에는 partition_expiration_days = 60 옵션을 사용해 자동 만료 파티션까지 생성합니다. :contentReference[oaicite:1]{index=1}
■ 내가 이해한 핵심
이번 실습의 본질은 아래 한 줄입니다.
날짜 필터를 쓰는 것과 날짜 파티션을 쓰는 것은 전혀 다르다
비파티션 테이블은 WHERE절에 날짜가 있어도 전체를 읽고 나중에 필터링합니다. 반면 날짜 파티션 테이블은 처음부터 날짜별로 저장 구간이 나뉘어 있기 때문에, 필요한 날짜 조각만 읽을 수 있습니다.
즉,
- WHERE절만 있으면: 나중에 걸러냄
- PARTITION BY가 있으면: 처음부터 필요한 조각만 읽음
이 차이가 곧 비용 차이입니다.
■ 내가 실제로 겪은 문제
처음에는 “없는 날짜를 조회하면 처리량도 거의 0이겠지”라고 생각했습니다.
하지만 비파티션 테이블에서는 결과가 0건이어도 여전히 1.74GB를 처리합니다. 실습 문서가 정확히 이 점을 짚습니다. 엔진은 실행 전에 해당 날짜 데이터가 있는지 모르기 때문에, 비파티션 테이블 전체를 확인해야 합니다. :contentReference[oaicite:2]{index=2}
이 경험으로 확실히 이해했습니다.
결과 행 수와 처리 바이트 수는 다르다
LIMIT은 결과를 줄일 뿐, 비파티션 테이블의 스캔량을 줄여주지 않는다
실습 단계
1단계. 실습용 Dataset 생성
목적: 내가 직접 만들 파티션 테이블을 저장할 작업 공간을 먼저 만드는 단계입니다.
BigQuery Explorer에서 프로젝트명 오른쪽 메뉴를 열고 CREATE DATASET을 선택합니다. Dataset ID는 ecommerce로 입력하고, 나머지 옵션은 기본값으로 둡니다. 생성이 끝나면 왼쪽 리소스 트리에 ecommerce가 보여야 합니다. 실습 문서도 같은 방식으로 Dataset을 먼저 만들도록 안내합니다. :contentReference[oaicite:3]{index=3}
이 단계가 중요한 이유는 원본 공개 데이터와 내가 직접 생성하는 파티션 테이블을 구분해서 다뤄야 하기 때문입니다. 같은 날짜 조건 쿼리라도 어느 테이블에 실행하느냐에 따라 처리량이 완전히 달라집니다.
2단계. 비파티션 테이블에서 날짜 조건 조회 비용 확인
목적: 파티션이 없을 때 BigQuery가 얼마나 비효율적으로 동작하는지 먼저 확인하는 단계입니다.
아래 쿼리를 실행합니다.
#standardSQL
SELECT DISTINCT
fullVisitorId,
date,
city,
pageTitle
FROM `data-to-insights.ecommerce.all_sessions_raw`
WHERE date = '20170708'
LIMIT 5
이 쿼리는 결과는 5건만 반환하지만, 실습 문서 기준으로 예상 처리량은 1.74GB입니다. 즉, LIMIT 5를 썼다고 해서 5건만 읽는 것이 아닙니다. BigQuery는 해당 날짜가 어디 있는지 알기 위해 전체를 훑습니다. :contentReference[oaicite:4]{index=4}
여기서 초보자가 자주 하는 오해는 두 가지입니다.
- LIMIT이 스캔량을 줄여줄 것이다
- 날짜 WHERE절이 있으니 날짜 부분만 읽을 것이다
둘 다 비파티션 테이블에서는 맞지 않을 수 있습니다.
3단계. 없는 날짜를 조회해도 왜 같은 비용이 드는지 확인
목적: 결과가 0건이어도 비파티션 테이블에서는 전체 스캔이 발생한다는 점을 확인하는 단계입니다.
이번에는 존재하지 않는 날짜를 조회합니다.
#standardSQL
SELECT DISTINCT
fullVisitorId,
date,
city,
pageTitle
FROM `data-to-insights.ecommerce.all_sessions_raw`
WHERE date = '20180708'
LIMIT 5
실습 문서에 따르면 이 쿼리도 여전히 1.74GB를 처리합니다. 결과는 0건인데도 그렇습니다. 이유는 엔진이 “결과 없음”을 판단하기 위해서도 전체를 확인해야 하기 때문입니다. :contentReference[oaicite:5]{index=5}
이 단계에서 꼭 기억해야 할 핵심은 아래와 같습니다.
비파티션 테이블에서는 없는 날짜를 확인하는 비용도 비싸다
4단계. 날짜 문자열을 DATE로 변환해 파티션 테이블 생성
목적: 원본 문자열 날짜를 실제 DATE 타입으로 바꾸고, 이를 기준으로 날짜별 파티션 테이블을 만드는 단계입니다.
아래 쿼리를 실행합니다.
#standardSQL
CREATE OR REPLACE TABLE ecommerce.partition_by_day
PARTITION BY date_formatted
OPTIONS(
description="a table partitioned by date"
) AS
SELECT DISTINCT
PARSE_DATE("%Y%m%d", date) AS date_formatted,
fullvisitorId
FROM `data-to-insights.ecommerce.all_sessions_raw`
이 쿼리에서 가장 중요한 부분은 두 가지입니다.
PARSE_DATE("%Y%m%d", date): 문자열 날짜를 DATE 타입으로 변환PARTITION BY date_formatted: 변환한 날짜를 기준으로 파티션 생성
실습 문서도 날짜 문자열은 바로 파티션 기준으로 쓰지 않고, PARSE_DATE로 변환한 새 컬럼을 사용합니다. :contentReference[oaicite:6]{index=6}
생성 후 테이블 ecommerce.partition_by_day의 Details 탭에서 다음을 확인합니다.
- Partitioned by: Day
- Partitioning on:
date_formatted
이 단계가 끝나면 테이블 구조가 완전히 바뀝니다. 더 이상 “전체 덩어리”가 아니라 날짜별로 잘린 저장 구조가 됩니다.
5단계. 파티션 테이블에서 실제 처리량 감소 확인
목적: 날짜 파티션이 실제로 비용 절감 효과가 있는지 숫자로 확인하는 단계입니다.
아래 쿼리를 실행합니다.
#standardSQL
SELECT *
FROM `data-to-insights.ecommerce.partition_by_day`
WHERE date_formatted = '2016-08-01'
실습 문서 기준으로 이 쿼리는 약 25KB 정도만 처리합니다. 앞서 비파티션 테이블이 1.74GB를 읽던 것과 비교하면 차이가 압도적입니다. :contentReference[oaicite:7]{index=7}
왜 이렇게 줄어드느냐가 핵심입니다. 이제 BigQuery는 전체를 볼 필요가 없습니다. 2016-08-01 파티션만 읽으면 되기 때문입니다.
즉, 같은 날짜 조건 쿼리라도
- 비파티션: 전체 스캔 후 필터
- 파티션: 필요한 날짜 조각만 읽음
이 차이가 곧 비용 차이입니다.
6단계. 존재하지 않는 날짜 파티션 조회 시 0B 처리 확인
목적: 파티션 구조에서는 없는 날짜를 찾는 비용도 거의 사라진다는 점을 확인하는 단계입니다.
아래 쿼리를 실행합니다.
#standardSQL
SELECT *
FROM `data-to-insights.ecommerce.partition_by_day`
WHERE date_formatted = '2018-07-08'
실습 문서에 따르면 이 쿼리는 0B 처리로 표시됩니다. 엔진이 파티션 메타데이터만 보고 해당 날짜 파티션이 없다는 사실을 곧바로 알기 때문입니다. :contentReference[oaicite:8]{index=8}
이 단계는 운영 환경에서 정말 중요합니다. 최근 7일 데이터, 특정 월 데이터, 전일 로그처럼 날짜 기반 조회가 반복되는 시스템에서는 이런 구조 차이가 누적 비용을 크게 바꿉니다.
7단계. 자동 만료 파티션 테이블 만들기
목적: 오래된 파티션을 자동 삭제해 저장비용과 보존 정책을 함께 관리하는 구조를 익히는 단계입니다.
실습 문서는 NOAA 공개 데이터를 기반으로 최근 강수 데이터만 유지하는 테이블을 생성합니다. 핵심은 partition_expiration_days = 60 옵션입니다.
#standardSQL
CREATE OR REPLACE TABLE ecommerce.days_with_rain
PARTITION BY date
OPTIONS (
partition_expiration_days=60,
description="weather stations with precipitation, partitioned by day"
) AS
SELECT
DATE(CAST(year AS INT64), CAST(mo AS INT64), CAST(da AS INT64)) AS date,
(SELECT ANY_VALUE(name) FROM
`bigquery-public-data.noaa_gsod.stations` AS stations
WHERE stations.usaf = stn) AS station_name,
prcp
FROM `bigquery-public-data.noaa_gsod.gsod*` AS weather
WHERE prcp < 99.9
AND length(_TABLE_SUFFIX) = 4
AND CAST(_TABLE_SUFFIX AS int64) >= 2018
AND prcp > 0
AND CAST(_TABLE_SUFFIX AS int64) >= 2018
이 단계가 중요한 이유는 단순 비용 최적화를 넘어 운영 정책과 연결되기 때문입니다.
- 오래된 로그 자동 삭제
- 개인정보 보관 기간 관리
- 불필요한 저장비용 방지
즉, 파티션은 조회 성능만이 아니라 데이터 수명주기 관리 도구이기도 합니다. :contentReference[oaicite:9]{index=9}
8단계. 파티션 만료가 실제로 반영되는지 검증
목적: 자동 만료 옵션이 실제로 테이블에 반영되었는지 확인하는 단계입니다.
아래 쿼리를 실행합니다.
#standardSQL
SELECT
AVG(prcp) AS average,
station_name,
date,
CURRENT_DATE() AS today,
DATE_DIFF(CURRENT_DATE(), date, DAY) AS partition_age,
EXTRACT(MONTH FROM date) AS month
FROM ecommerce.days_with_rain
WHERE station_name = 'WAKAYAMA'
GROUP BY station_name, date, today, month, partition_age
ORDER BY partition_age DESC
여기서 핵심은 partition_age입니다. 실습 문서는 가장 오래된 파티션 나이가 60일 이하인지 확인하라고 안내합니다. 즉, “이론상 60일 만료”가 아니라 실제로 살아남은 파티션이 60일 이내인지 검증하는 단계입니다. :contentReference[oaicite:10]{index=10}
좋은 데이터 엔지니어링은 생성에서 끝나지 않습니다. 생성 후 설정이 실제로 반영됐는지 검증하는 과정까지 포함됩니다.
실습 증거
1. 비파티션 테이블의 1.74GB 처리
실습 문서는 2017년 특정 날짜를 조회하는 쿼리도, 존재하지 않는 2018년 날짜를 조회하는 쿼리도 비파티션 테이블에서는 모두 1.74GB를 처리한다고 설명합니다. 이는 WHERE절만으로는 스캔량이 줄지 않는다는 직접 증거입니다. :contentReference[oaicite:11]{index=11}
2. 파티션 테이블 생성 후 Day 파티션 확인
ecommerce.partition_by_day 테이블의 Details 탭에서 Partitioned by: Day와 Partitioning on: date_formatted를 확인할 수 있습니다. 이는 실제로 날짜 파티션 구조가 적용되었음을 보여줍니다. :contentReference[oaicite:12]{index=12}
3. 파티션 테이블 조회 시 약 25KB 처리
date_formatted = '2016-08-01' 조건으로 파티션 테이블을 조회했을 때 처리량이 약 25KB로 줄어듭니다. 비파티션 1.74GB와의 차이가 이 실습의 핵심 증거입니다. :contentReference[oaicite:13]{index=13}
4. 존재하지 않는 날짜 조회 시 0B 처리
date_formatted = '2018-07-08' 조회에서 처리량이 0B로 표시됩니다. 이는 파티션 메타데이터만으로 없는 날짜를 판별할 수 있음을 보여주는 직접 증거입니다. :contentReference[oaicite:14]{index=14}
트러블슈팅
문제 증상:
날짜 WHERE절을 넣었는데도 예상보다 처리량이 너무 크다.
원인 분석:
테이블이 날짜 파티션되어 있지 않아 전체를 읽고 나중에 필터링하기 때문이다.
확인 방법:
실행 전 예상 처리량이 날짜 조건과 무관하게 크게 보이는지 확인한다.
해결 방법:
문자열 날짜를 PARSE_DATE로 DATE 타입으로 바꾸고, PARTITION BY를 적용한 새 테이블을 만든다.
재발 방지 방법:
날짜 기반 로그, 이벤트, 주문 테이블은 처음부터 파티션 전략을 함께 설계한다.
문제 증상:
결과가 0건인데도 처리량이 크다.
원인 분석:
비파티션 테이블에서는 없는 날짜인지 확인하기 위해서도 전체 스캔이 필요하기 때문이다.
확인 방법:
존재하지 않는 날짜를 조회해도 처리 바이트 수가 원본 전체 크기와 비슷한지 본다.
해결 방법:
날짜 파티션 테이블로 전환한다.
재발 방지 방법:
결과 건수와 처리 바이트 수를 항상 분리해서 해석한다.
문제 증상:
오래된 데이터가 계속 남아 저장비용이 늘어난다.
원인 분석:
파티션 만료 옵션이 설정되지 않았거나 보존 정책이 수동 관리 상태다.
확인 방법:DATE_DIFF(CURRENT_DATE(), date, DAY)로 파티션 나이를 확인한다.
해결 방법:partition_expiration_days 옵션을 설정한다.
재발 방지 방법:
운영 테이블은 생성 시점부터 보존 기간 정책을 함께 설계한다.
실무 핵심 포인트
이번 실습의 핵심은 아래 한 문장으로 정리할 수 있습니다.
BigQuery 비용 최적화의 핵심은 쿼리 문장보다 테이블 설계다
SQL을 조금 더 짧게 쓰거나 예쁘게 쓴다고 비용이 크게 줄지 않을 수 있습니다. 하지만 날짜 파티션을 올바르게 설계하면 같은 날짜 조건 조회라도 비용 구조가 완전히 달라집니다.
특히 아래 같은 경우에는 파티션이 거의 필수에 가깝습니다.
- 최근 7일 로그 조회
- 지난달 매출 집계
- 특정 날짜 이벤트 분석
- 최근 60일만 보관하는 운영 정책
즉, 파티션은 고급 옵션이 아니라 날짜 기반 데이터를 다루는 기본 설계 원칙입니다.
결론
핵심 원칙
날짜 조건으로 자주 조회하는 BigQuery 테이블은 반드시 날짜 파티션을 검토해야 한다.
실무 적용 시 주의점
- 문자열 날짜는 바로 파티션 키로 쓰기 어렵기 때문에
PARSE_DATE같은 변환이 필요하다 - LIMIT은 결과 행 수를 줄일 뿐, 비파티션 테이블의 스캔량을 줄여주지 않는다
- 없는 날짜를 조회하는 패턴도 파티션 여부에 따라 비용 차이가 크다
- 개인정보나 단기 로그는
partition_expiration_days를 함께 설계해야 한다
다음 학습 단계 제안
다음에는 날짜 파티션 위에 클러스터링까지 추가해서, 특정 사용자나 특정 지역 기준 조회 비용을 더 줄이는 구조를 실습해보면 좋습니다.
'서버 구축·실습' 카테고리의 다른 글
| BigQuery에서 JOIN 오류가 발생하는 이유와 해결 방법 (데이터 조인 실습 완전 가이드) (0) | 2026.03.26 |
|---|---|
| BigQuery에서 JSON·ARRAY·STRUCT를 다루는 방법: 중첩 데이터 완전 이해 실습 (0) | 2026.03.26 |
| GCP Vision API로 이미지에서 텍스트 추출하고 번역하는 방법 (OCR 실습 완전 가이드) (0) | 2026.03.25 |
| GCP AutoML로 이미지 분류 모델 만드는 방법 (Cloud Vision API 실습 기반 완전 가이드) (0) | 2026.03.25 |
| GCP BigQuery에서 CSV·GCS·Google Sheets 데이터를 적재하는 방법 (실습 기반 완전 가이드) (0) | 2026.03.24 |