# GA4 raw 데이터에서 평탄화 마트로

GA4 BigQuery export는 event_params·user_properties·items가 모두 RECORD/ARRAY 중첩 구조라, 그대로는 단순 페이지뷰 카운트에도 UNNEST가 필요하고 Looker Studio·Sheets 같은 BI 도구로 다룰 수 없습니다. 평탄화(flattening) 마트가 어떤 문제를 푸는지, 그리고 평탄화하지 않아도 되는 경우는 언제인지 정리합니다.

- 카테고리: BigQuery
- 소요 시간: 약 8분
- 난이도: 입문
- 업데이트: 2026.05.28
- 원문: /wiki/playbook/bigquery-checklist/why-flatten-ga4-bigquery-data

## 목차

- [raw export는 어떤 모양인가](#raw-structure)
- [그대로 분석하면 부딪히는 네 가지](#problems)
- [평탄화 마트가 푸는 문제](#flat-mart)
- [평탄화의 부수효과](#side-effects)
- [평탄화하지 않아도 되는 경우](#when-not)
- [자주 묻는 질문](#faq)

GA4 BigQuery export는 일별 `events_YYYYMMDD` 테이블로 들어오지만, 그 안의 `event_params`·`user_properties`·`items`가 모두 **RECORD/ARRAY 중첩 구조**입니다. 그대로는 단순한 페이지뷰 카운트 한 줄에도 `UNNEST()`가 필요하고, Looker Studio·Sheets 같은 BI 도구는 중첩 구조를 풀지 못합니다. 이 글은 raw 데이터가 어떤 모양인지, 분석 운영에서 왜 그대로 쓰기 어려운지, 그리고 **평탄화(flattening) 마트**가 어떤 문제를 푸는지를 정리합니다. 실제 평탄화 마트의 정의와 SQL은 시리즈 후반의 **Event_Flat / Item_Performance** 글에서 다룹니다.

## raw export는 어떤 모양인가

[**BigQuery로 들어오는 GA4 데이터**](/wiki/playbook/bigquery-checklist/what-is-ga4-data-in-bigquery) 에서 자세히 다뤘듯, GA4 BigQuery export는 매일 `events_YYYYMMDD` 일별 테이블 하나가 추가되는 형태입니다. 한 행이 한 이벤트, 컬럼은 약 30~50개. 이 중 평평한 컬럼(`event_name`·`event_timestamp`·`user_pseudo_id` 등)은 그대로 다루기 쉽지만, 핵심 분석 컬럼 세 개가 모두 **중첩 배열**로 들어옵니다.

| 중첩 컬럼 | 실제 들어 있는 모양 | 여기에 담기는 정보 |
| --- | --- | --- |
| `event_params` | ARRAY of STRUCT(key, value<string_value, int_value, float_value, double_value>) | 페이지 URL·세션 ID·페이지 제목 등 거의 모든 동적 파라미터 |
| `user_properties` | ARRAY of STRUCT(key, value, set_timestamp_micros) | 로그인 사용자 ID·회원 등급·실험 그룹 같은 사용자 속성 |
| `items` | ARRAY of STRUCT(item_id, item_name, price, quantity,...) | 이커머스 상품 라인업 — 한 이벤트에 여러 상품이 함께 들어감 |

이 ‘배열 안의 STRUCT’ 형태는 단일 이벤트에 가변 개수의 정보를 담아 보내려는 GA4의 설계상 자연스럽습니다. 다만 분석할 땐 “`page_location` 값 좀 꺼내 줘”라는 한 줄 질문에도 SQL이 길어집니다.

### 실제로는 BigQuery 미리보기에서 이렇게 보입니다

한 이벤트(예: `page_view` 1건)는 평탄한 컬럼 1줄 + `event_params` 안에 여러 개의 key/value 줄을 함께 갖습니다. 같은 `event_timestamp`를 공유하는 줄이 여러 개 늘어선 모양이라고 보면 됩니다.

| 행 | event_name | event_params.key | event_params.value.string_value | event_params.value.int_value |
| --- | --- | --- | --- | --- |
| **1** | `page_view` | page_location | https://hurdlers.kr/blog/ga4 | (null) |
| page_title | Hurdlers Blog | (null) |  |  |
| ga_session_id | (null) | 1700000001 |  |  |
| **2** | `scroll` | page_location | https://hurdlers.kr/blog/ga4 | (null) |
| percent_scrolled | (null) | 90 |  |  |

한 이벤트가 **한 줄**이 아니라 **‘평탄 컬럼 + key/value 배열 줄 여러 개’**로 흩어져 있다는 게 핵심입니다. 그리고 각 value는 문자열/정수/실수/double 중 어디에 들어가 있는지에 따라 `value.string_value`·`value.int_value` 식으로 따로 꺼내야 합니다.

## 그대로 분석하면 부딪히는 네 가지

### ① 단순한 질문에도 UNNEST가 줄줄이

“어제 페이지뷰가 가장 많은 URL 다섯 개”라는 평범한 분석을 raw 데이터에서 하려면 다음처럼 `UNNEST()` 절을 만들어 각 파라미터를 꺼냅니다.

**raw 데이터에서 page_location 꺼내기**
```sql
SELECT
  (SELECT value.string_value
   FROM UNNEST(event_params)
   WHERE key = 'page_location') AS page_location,
  COUNT(*) AS pv
FROM `hurdlers-web.analytics_502771866.events_20260527`
WHERE event_name = 'page_view'
GROUP BY page_location
ORDER BY pv DESC
LIMIT 5;
```

파라미터를 두세 개만 더 끌어오면 서브쿼리가 세 줄, 네 줄로 늘어나고, 같은 `UNNEST(event_params)`를 여러 번 적게 됩니다. 운영 분석에서 이런 쿼리를 매번 새로 쓰는 건 비현실적입니다.

### ② Looker Studio·Sheets는 중첩 컬럼을 풀지 못합니다

BI 도구는 보통 ‘행 = 1 이벤트, 컬럼 = 평탄한 값’을 가정합니다. raw `events_` 테이블을 Looker Studio에 그대로 연결하면 `event_params` 컬럼이 “RECORD” 타입으로만 보이고 안의 `page_location`·`session_id` 같은 값에는 접근할 수 없습니다. 결과적으로 **SQL을 쓸 줄 아는 사람만 GA4 raw 데이터에 손이 닿는** 구조가 됩니다.

### ③ 매번 같은 UNNEST를 반복해 비용·시간이 누적

BigQuery 쿼리 비용은 ‘실제로 읽은 데이터 양(scanned bytes)’으로 매겨집니다. raw 테이블에서 `event_params`를 `UNNEST`하면 일별 테이블 전체의 array 컬럼을 스캔하는데, 같은 질문을 매일 새로 돌리면 이 비용이 매번 다시 발생합니다. 자주 쓰는 파라미터를 미리 평탄한 컬럼으로 빼 두면 쿼리 한 번이 *“필요한 컬럼만 읽고 끝”*이 됩니다.

### ④ 사람마다 다른 SQL → 사람마다 다른 답

같은 “page_view 수”라도 작성자가 *“event_name = 'page_view'를 어떻게 거를지”*·*“session_engaged 조건을 넣을지”*·*“봇 트래픽을 어디서 빼낼지”*를 다르게 잡으면 다른 결과가 나옵니다. 마트는 **‘한 번 정의해 두면 모두 같은 답을 본다’**는 단일 진실 공급원(single source of truth) 역할을 합니다.

> **주의**
>
> **네 한계는 따로 떨어진 게 아닙니다.** 운영 분석은 ‘똑같은 정의를, 매일, 여러 사람이, 여러 도구로’ 돌려야 합니다. raw 그대로는 이 네 조건이 한꺼번에 부서지기 때문에 평탄화가 거의 필수가 됩니다.

## 평탄화 마트가 푸는 문제

평탄화 마트는 “자주 쓰는 파라미터를 미리 평탄한 컬럼으로 펼쳐 두는” 테이블입니다. raw 테이블을 매일 한 번 읽어서 풀어 두고, 이후 분석은 이 마트만 봅니다. 같은 두 이벤트를 `Event_Flat`에 펼쳐 두면 다음과 같은 모양이 됩니다.

| 행 | event_name | page_location | page_title | ga_session_id | percent_scrolled |
| --- | --- | --- | --- | --- | --- |
| **1** | `page_view` | https://hurdlers.kr/blog/ga4 | Hurdlers Blog | 1700000001 | (null) |
| **2** | `scroll` | https://hurdlers.kr/blog/ga4 | (null) | 1700000001 | 90 |

한 이벤트가 정확히 한 줄로 보이고, 자주 쓰는 키들은 모두 자기 자리에 펼쳐져 있습니다. raw에서 5줄이었던 데이터가 마트에서는 2줄로, 그리고 `UNNEST` 없이 어떤 도구로도 곧장 다룰 수 있는 형태가 됩니다.

위 ①의 같은 질문(“어제 페이지뷰가 가장 많은 URL 다섯 개”)을 평탄화 마트(`Event_Flat`) 위에서 다시 써 보면:

**Event_Flat 위에서 같은 질문 다시 쓰기**
```sql
SELECT
  page_location,
  COUNT(*) AS pv
FROM `hurdlers-web.mart.Event_Flat`
WHERE event_date = '2026-05-27'
  AND event_name = 'page_view'
GROUP BY page_location
ORDER BY pv DESC
LIMIT 5;
```

서브쿼리·`UNNEST`가 사라지고, 평탄한 컬럼만 골라 읽으니 스캔량도 작습니다. 같은 마트를 Looker Studio·Sheets에 그대로 연결할 수 있어 비 SQL 사용자도 같은 정의를 본 차트를 만듭니다.

### raw vs 평탄화 마트 — 분석 측면 비교

SQL 길이(같은 질문 기준)**약 8~12줄 → 약 5줄**

BI 도구 연결**raw 불가 → 마트 가능**

쿼리 스캔량**array 전체 스캔 → 평탄 컬럼만 스캔**

지표 정의 일관성**작성자별 차이 → 마트 정의서 = 단일 진실**

마트 정의서(스키마·컬럼 의미·갱신 주기)는 시리즈의 **테이블 정의서는 왜 필요한가** 글에서 별도로 다룹니다.

## 평탄화의 부수효과

- **정의서 ↔ 태그 사양 1:1 매핑** — 마트의 컬럼 이름·타입이 그대로 ‘우리가 수집한 파라미터 카탈로그’가 됩니다. 신규 파라미터를 추가할 때 “먼저 정의서 → 그 다음 태그 구현 → 마지막 평탄화 컬럼 추가”라는 절차가 자연스럽게 자리 잡습니다.
- **비개발 직군도 BigQuery에 접근** — 평탄한 컬럼 구조면 마케터·기획자가 BigQuery Studio에서 `SELECT page_location,...` 정도의 쿼리만 써도 인사이트를 뽑을 수 있습니다.
- **지표 계보 추적** — Looker Studio 차트 → 마트 컬럼 → 정의서 → 태그 사양 → 실제 코드까지 한 줄로 따라갈 수 있어, 숫자가 이상할 때 어디서 어긋났는지 빠르게 짚어낼 수 있습니다.
- **모델·실험 입력** — A/B 테스트 결과 집계, 추천 모델 학습 데이터, LTV 모델 입력 같은 데이터 사이언스 작업도 평탄한 마트 위에서 한 줄 SQL로 시작할 수 있습니다.

## 평탄화하지 않아도 되는 경우

모든 분석이 마트로 갈 필요는 없습니다. 다음 같은 상황은 raw로 바로 다루는 편이 더 합리적입니다.

- **1회성 ad-hoc 분석** — 한 번만 답을 보면 되는 질문은 마트 컬럼을 늘리기보다 raw에서 `UNNEST` 한 번으로 끝내는 편이 빠릅니다.
- **아직 정형화되지 않은 새 파라미터 탐색** — 새로 수집하기 시작한 파라미터가 데이터에 안정적으로 들어오는지 먼저 raw에서 확인한 뒤, 충분히 안정되면 마트 컬럼으로 승격합니다.
- **매우 작은 규모** — 일 1,000건 미만의 사이트라면 raw 전체를 매번 스캔해도 비용·시간이 거의 들지 않아 굳이 마트를 만들지 않아도 됩니다.
- **장기 보관용 raw 자체** — raw `events_YYYYMMDD`는 ‘원본의 원본’으로 평탄화 여부와 무관하게 그대로 보관합니다. 마트는 raw 위의 가공 결과일 뿐 raw를 대체하지 않습니다.

정리하면 — **운영 분석은 평탄화 마트로, 탐색은 raw로**. 두 층을 함께 두면 ‘일상 분석’과 ‘새 발견’이 충돌하지 않습니다.

## 자주 묻는 질문

### 평탄화하면 저장 비용이 두 배로 늘지 않나요?

실제로 늘긴 하지만 두 배까지는 아닙니다. raw `events_`에 있는 `event_params`·`user_properties` 안에서 자주 쓰는 키 10~30개만 평탄한 컬럼으로 펼쳐 두는 게 일반적이라, 마트는 raw 대비 0.3~0.7배 정도의 압축 후 크기가 됩니다. 또 마트 테이블에 90일 자동 만료를 걸어 두면 장기 보관이 필요한 건 raw, 단기 분석용은 마트로 자연스럽게 분리됩니다.

### 어떤 파라미터를 평탄화해야 하나요?

대시보드·정기 리포트에서 자주 등장하는 키를 우선합니다. 일반적으로 `page_location`·`page_title`·`ga_session_id`·`ga_session_number`·`session_engaged`·`engagement_time_msec`· GCLID·UTM 5종이 1순위. 이커머스라면 `items` 풀어서 `Item_Performance` 마트로 분리하는 것까지가 표준입니다.

### 평탄화 마트는 매번 손으로 SQL을 돌려야 하나요?

아닙니다. **예약 쿼리(Scheduled Queries)**를 등록해 GA4 export가 들어온 직후 자동으로 마트를 갱신합니다. 이 자동화 흐름은 시리즈의 **GA4 export 시점에 예약쿼리 업데이트하는 방법** 글에서 별도로 다룹니다. 한 번 등록해 두면 사람이 손을 댈 일이 없습니다.

### raw 데이터는 그래도 보관해야 하나요?

네. raw `events_YYYYMMDD`는 단일 진실 공급원의 원본입니다. 마트는 raw에서 뽑아낸 가공 결과이고, 새로 평탄화 컬럼이 필요해지면 raw로 돌아가 다시 만듭니다. raw를 지우면 과거를 다시 만들 수 없으니 그대로 두는 게 원칙입니다(샌드박스 60일 만료 제거가 그래서 중요 — 자세히는 [**무료 버전(샌드박스) 해제해야 하는 이유**](/wiki/playbook/bigquery-checklist/why-upgrade-from-bigquery-sandbox) 참고).

### 평탄화 컬럼을 나중에 추가하려면 어떻게 하나요?

마트 정의서에 새 컬럼을 먼저 추가하고, 예약 쿼리 SQL의 SELECT 절에 그 컬럼을 끄집어내는 식을 더한 뒤, raw에서 과거 분량을 한 번 backfill 합니다. 과거 데이터를 함께 채우면 새 컬럼이 이전 날짜에도 정상적으로 보입니다.

### UNNEST를 잘 쓰면 평탄화 안 해도 되지 않나요?

SQL을 잘 쓰는 사람이 혼자 분석하는 환경이라면 그렇습니다. 다만 운영 분석은 보통 **여러 사람·여러 도구·매일 같은 정의**가 요구돼서, “나만 잘 쓰면 된다”로는 일관성이 무너집니다. 평탄화 마트는 SQL 실력이 아니라 ‘정의를 공유 가능한 형태로 두는 일’에 가깝습니다.

## Navigation

- [전체 플레이북 Markdown sitemap](/wiki/playbook/sitemap.md)

### BigQuery

- [BigQuery](/wiki/playbook/bigquery-checklist)
- [BigQuery 온보딩](/wiki/playbook/bigquery-checklist/bigquery-onboarding-guide)
- [BigQuery 인증 설정](/wiki/playbook/bigquery-checklist/how-to-set-up-bigquery-authentication)
- [BigQuery 데이터셋·테이블 설계](/wiki/playbook/bigquery-checklist/bigquery-dataset-and-table-design-guide)
- [BigQuery 쿼리·비용 제어](/wiki/playbook/bigquery-checklist/bigquery-query-and-cost-control-guide)
- [BigQuery 쓰기·MERGE·CDC](/wiki/playbook/bigquery-checklist/bigquery-data-write-and-cdc-guide)
- [BigQuery 코드 예제 모음](/wiki/playbook/bigquery-checklist/bigquery-code-examples)
- [BigQuery로 들어오는 GA4 데이터](/wiki/playbook/bigquery-checklist/what-is-ga4-data-in-bigquery)
- [BigQuery Studio 인터페이스 이해하기](/wiki/playbook/bigquery-checklist/what-is-bigquery-studio-interface)
- [BigQuery 예상 비용](/wiki/playbook/bigquery-checklist/how-to-estimate-bigquery-costs)
- [무료 버전(샌드박스) 해제해야 하는 이유](/wiki/playbook/bigquery-checklist/why-upgrade-from-bigquery-sandbox)
- [GA4 BigQuery 데이터를 왜 평탄화해야 하나](/wiki/playbook/bigquery-checklist/why-flatten-ga4-bigquery-data) (현재 문서)
- [Log Router란?](/wiki/playbook/bigquery-checklist/what-is-log-router)
- [Pub/Sub이란?](/wiki/playbook/bigquery-checklist/what-is-pubsub)
- [Cloud Functions이란?](/wiki/playbook/bigquery-checklist/what-is-cloud-function-and-run)
- [GA4 export 시점에 예약 쿼리 자동 실행하기](/wiki/playbook/bigquery-checklist/how-to-trigger-scheduled-query-on-ga4-export)
- [테이블 정의서는 왜 필요한가](/wiki/playbook/bigquery-checklist/why-table-definition-doc)
- [Event_Flat 테이블 이해하기](/wiki/playbook/bigquery-checklist/what-is-event-flat-table)
- [Item_Performance 테이블 이해하기](/wiki/playbook/bigquery-checklist/what-is-item-performance-table)
