# 한 행 = 한 상품 라인으로 펼친 이커머스 마트

GA4 이커머스 이벤트의 상품(item) 단위 마트 Item_Performance를 정리합니다. 한 이벤트에 여러 상품이 담기는 items 배열 때문에 Event_Flat(한 행 = 한 이벤트)과 단위가 충돌해 따로 두는 이유, UNNEST(items)로 상품 라인을 펼친 한 행 = 한 상품 구조, 공통 컬럼 + 표준 상품 필드 + 커스텀 item_params 예시, 그리고 스키마 생성·예약쿼리·백필 쿼리 3종까지 비개발자도 따라올 수 있게 정리합니다.

- 카테고리: BigQuery
- 소요 시간: 약 8분
- 난이도: 입문
- 업데이트: 2026.06.05
- 원문: /wiki/playbook/bigquery-checklist/what-is-item-performance-table

## 목차

- [왜 Event_Flat과 분리했나](#why-separate)
- [한 행 = 한 상품 라인](#grain)
- [공통 + 상품 컬럼 + 커스텀 예시](#schema)
- [제공 쿼리 3종 — 스키마·예약쿼리·백필](#queries)
- [Item_Performance로 답하는 질문들](#usage)
- [자주 묻는 질문](#faq)

`Item_Performance`는 GA4 이커머스 이벤트의 **상품(item) 단위** 마트입니다 — [Event_Flat](/wiki/playbook/bigquery-checklist/what-is-event-flat-table)이 ‘한 행 = 한 이벤트’라면, 이쪽은 ‘한 행 = 한 상품 라인’이죠. 이 글은 왜 이 테이블을 Event_Flat에서 떼어 따로 두는지, 어떤 컬럼으로 구성되는지, 그리고 그 테이블을 만들고 채우는 **스키마 생성·예약쿼리·백필** 쿼리 3종을 정리합니다. 컬럼 의미 관리는 [테이블 정의서](/wiki/playbook/bigquery-checklist/why-table-definition-doc), 자동 갱신은 [예약 쿼리 자동 실행](/wiki/playbook/bigquery-checklist/how-to-trigger-scheduled-query-on-ga4-export) 글을 함께 보세요.

## 왜 Event_Flat과 분리했나

한 번의 `purchase`(또는 `view_item`·`add_to_cart`) 이벤트에는 상품이 **여러 개** 함께 담깁니다. GA4 raw에서 이 상품 목록은 `items`라는 **배열(ARRAY)** 컬럼에 들어 있습니다. 즉 한 이벤트 안에 상품 1개일 수도, 10개일 수도 있죠.

이 `items` 배열을 Event_Flat에 넣으려면 둘 중 하나인데, 둘 다 문제가 생깁니다.

| 방법 | 무엇이 문제인가 |
| --- | --- |
| items를 배열 컬럼 그대로 둔다 | 다시 `UNNEST`가 필요해져 평탄화의 의미가 사라지고, BI 도구도 그 안을 못 풉니다. |
| items를 풀어(`UNNEST`) 한 줄씩 펼친다 | 상품 3개짜리 구매 1건이 **3행**이 됩니다. 그러면 Event_Flat의 ‘한 행 = 한 이벤트’가 깨져 **이벤트·세션 수가 부풀려집니다**(같은 구매가 3번 세짐). |

그래서 **단위(grain)가 다른 두 표**로 나눕니다 — 이벤트·세션을 세는 `Event_Flat`(한 행 = 한 이벤트)과, 상품을 세는 `Item_Performance`(한 행 = 한 상품 라인). 같은 데이터를 ‘무엇을 한 줄로 볼 것인가’만 다르게 펼친 셈입니다.

> **팁**
>
> **한 표에 단위 두 개를 섞으면 어느 숫자든 틀어집니다.** 이벤트를 세려고 만든 표에서 상품을 세거나, 그 반대를 하면 중복·누락이 생깁니다. ‘세는 대상이 다르면 표를 나눈다’가 마트 설계의 기본 원칙입니다.

## 한 행 = 한 상품 라인

상품 3개를 담은 `purchase` 이벤트 1건은 Item_Performance에서 3행이 됩니다. 이벤트·세션 컬럼은 세 행이 똑같이 공유하고, 상품 컬럼만 행마다 달라집니다.

| 행 | event_name | session_id | item_name | price | quantity | item_revenue |
| --- | --- | --- | --- | --- | --- | --- |
| **1** | `purchase` | u1·s9 | 티셔츠 | 29,000 | 1 | 29,000 |
| **2** | `purchase` | u1·s9 | 양말 | 5,000 | 2 | 10,000 |
| **3** | `purchase` | u1·s9 | 모자 | 19,000 | 1 | 19,000 |

그래서 ‘구매 건수’는 `ecommerce_transaction_id`를 `DISTINCT`로, ‘상품별 매출’은 `item_revenue`를 더해서 봅니다. 같은 표 안에 이벤트 요약값(`purchase_revenue`)과 상품값(`item_revenue`)이 함께 있으니 둘을 헷갈리지 않는 게 중요합니다.

## 공통 + 상품 컬럼 + 커스텀 예시

Item_Performance는 **Event_Flat과 같은 공통 컬럼**(이벤트·사용자/세션·기기·지역·거래요약·날짜· 유입·페이지)에 **GA4 표준 상품 필드**를 더하고, 필요하면 **커스텀 item_params**를 몇 개 붙입니다. 공통 컬럼은 [Event_Flat](/wiki/playbook/bigquery-checklist/what-is-event-flat-table)과 같고(페이지 묶음만 `page_title`·`page_url`로 단출), 그 아래에 상품 묶음이 더해집니다.

| 묶음 | 컬럼(타입) | 의미 |
| --- | --- | --- |
| 이벤트 | `event_name` (STRING) | 이벤트 이름 |
| 사용자·세션 | `user_id` (STRING) | 로그인 사용자 ID(있을 때만) |
| `user_pseudo_id` (STRING) | 익명 사용자 ID |  |
| `session_id` (STRING) | `user_pseudo_id` + `ga_session_id` 세션 키 |  |
| `session_engaged` (INT64) | 참여 세션 플래그 |  |
| `session_number` (INT64) | 사용자의 세션 순번 |  |
| `visit_type` (STRING) | new_visit / return_visit |  |
| 기기 | `device_category` · `device_os` · `device_browser` · `host_name` | 기기 분류 · OS · 브라우저 · 호스트 |
| 지역 | `country` · `city` · `region` (STRING) | 국가 · 도시 · 지역 |
| 거래 요약 | `ecommerce_transaction_id` (STRING) | 거래 ID |
| `purchase_revenue` (NUMERIC) | 이벤트 단위 구매 금액 |  |
| 날짜·시간 | `date` (DATE) · `date_time` (DATETIME) | 이벤트 날짜·일시(Asia/Seoul, date 파티션 기준) |
| 유입 | `cross_channel_source/medium/campaign` · `channel_group` | 크로스채널 기준 유입(기본 권장) |
| `manual_source/medium/campaign/content/term` | 수동(UTM) 기준 유입 |  |
| 페이지 | `page_title` · `page_url` (STRING) | 페이지 제목 · 전체 URL |
| 상품 식별·분류 | `item_id` · `item_name` (STRING) | 상품 ID · 상품명 |
| `item_brand` (STRING) | 브랜드 |  |
| `item_category` ~ `item_category5` (STRING) | 카테고리 1~5단계 |  |
| `item_variant` (STRING) | 옵션·변형(색상·사이즈 등) |  |
| `currency` (STRING) | 통화 |  |
| 금액·수량 | `price` (NUMERIC) | 상품 단가 |
| `quantity` (INT64) | 수량 |  |
| `item_revenue` (NUMERIC) | 상품 단위 매출 |  |
| 노출·목록 | `item_list_id` · `item_list_name` (STRING) | 노출된 목록 ID · 이름 |
| `item_index` (INT64) | 목록 내 위치 |  |
| 프로모션 | `promotion_id` · `promotion_name` (STRING) | 프로모션 ID · 이름 |
| `creative_slot` · `location_id` (STRING) | 크리에이티브 슬롯 · 위치 ID |  |
| 커스텀(예시) | `item_discount_rate` (FLOAT64) | 할인율 — `item.item_params.item_discount_rate` |
| `item_stock_status` (STRING) | 재고 상태 — `item.item_params.item_stock_status` |  |

> **팁**
>
> **커스텀 두 컬럼은 사이트마다 다릅니다.** 상품에 붙는 커스텀 값은 `item.item_params`에서 꺼냅니다(사용자 속성은 `user_properties`). 추가한 컬럼의 의미·원천은 [테이블 정의서](/wiki/playbook/bigquery-checklist/why-table-definition-doc)에 적어 둡니다.

## 제공 쿼리 3종 — 스키마·예약쿼리·백필

Event_Flat과 같은 3종 구조입니다([Event_Flat 글](/wiki/playbook/bigquery-checklist/what-is-event-flat-table)의 3종 참고). 핵심 차이는 변환 쿼리의 **`UNNEST(items)`** 와 **이커머스 이벤트 필터**입니다. 공통 컬럼도 축약 없이 모두 적었습니다.

### ① 스키마 생성

**Item_Performance.schema.sql**
```sql
CREATE OR REPLACE TABLE `[PROJECT_ID].[DATASET_ID].Item_Performance` (
  event_name STRING OPTIONS(description="GA4 이커머스 이벤트 이름"),

  -- 사용자 / 세션
  user_id STRING OPTIONS(description="로그인 사용자 ID(있을 때만)"),
  user_pseudo_id STRING OPTIONS(description="익명 사용자 ID"),
  session_id STRING OPTIONS(description="user_pseudo_id + ga_session_id로 만든 세션 키"),
  session_engaged INT64 OPTIONS(description="참여 세션 플래그"),
  session_number INT64 OPTIONS(description="사용자의 세션 순번"),
  visit_type STRING OPTIONS(description="new_visit / return_visit"),

  -- 기기 / 지역
  device_category STRING, device_os STRING, device_browser STRING, host_name STRING,
  country STRING, city STRING, region STRING,

  -- 거래 요약 / 날짜
  ecommerce_transaction_id STRING OPTIONS(description="거래 ID"),
  purchase_revenue NUMERIC OPTIONS(description="이벤트 단위 구매 금액"),
  date DATE OPTIONS(description="이벤트 날짜(Asia/Seoul)"),
  date_time DATETIME OPTIONS(description="이벤트 일시(Asia/Seoul)"),

  -- 유입
  cross_channel_source STRING, cross_channel_medium STRING, cross_channel_campaign STRING,
  channel_group STRING OPTIONS(description="크로스채널 기본 채널 그룹"),
  manual_source STRING, manual_medium STRING, manual_campaign STRING, manual_content STRING, manual_term STRING,

  -- 페이지
  page_title STRING, page_url STRING,

  -- GA4 표준 상품 필드
  item_id STRING, item_name STRING, item_brand STRING,
  item_category STRING, item_category2 STRING, item_category3 STRING,
  item_category4 STRING, item_category5 STRING, item_variant STRING,
  price NUMERIC OPTIONS(description="상품 단가"),
  quantity INT64 OPTIONS(description="수량"),
  item_revenue NUMERIC OPTIONS(description="상품 단위 매출"),
  currency STRING,
  item_list_id STRING, item_list_name STRING, item_index INT64,
  promotion_id STRING, promotion_name STRING, creative_slot STRING, location_id STRING,

  -- 커스텀 (예시: taxonomy에 맞게 추가)
  item_discount_rate FLOAT64 OPTIONS(description="할인율. item.item_params.item_discount_rate"),
  item_stock_status STRING  OPTIONS(description="재고 상태. item.item_params.item_stock_status")
)
PARTITION BY date
CLUSTER BY event_name, item_id, session_id
OPTIONS(description = "GA4 상품 단위 이커머스 마트");
```

### ② 예약쿼리(일간 적재)

`FROM events_*, UNNEST(items) AS item`로 상품 배열을 행으로 펼치고, 이커머스 이벤트만 거릅니다. 공통 컬럼 SELECT는 Event_Flat과 같고, 상품 컬럼은 `item.*`에서 바로 가져옵니다(커스텀만 `UNNEST(item.item_params)`).

**Item_Performance.transform.sql (예약쿼리)**
```sql
INSERT INTO `[PROJECT_ID].[DATASET_ID].Item_Performance`
SELECT
  event_name,

  -- 사용자 / 세션
  user_id,
  user_pseudo_id,
  CONCAT(user_pseudo_id,
    CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key='ga_session_id') AS STRING)) AS session_id,
  (SELECT value.int_value FROM UNNEST(event_params) WHERE key='session_engaged')   AS session_engaged,
  (SELECT value.int_value FROM UNNEST(event_params) WHERE key='ga_session_number') AS session_number,
  CASE
    WHEN (SELECT value.int_value FROM UNNEST(event_params) WHERE key='ga_session_number') = 1 THEN 'new_visit'
    WHEN (SELECT value.int_value FROM UNNEST(event_params) WHERE key='ga_session_number') > 1 THEN 'return_visit'
  END AS visit_type,

  -- 기기 / 지역
  device.category AS device_category,
  device.operating_system AS device_os,
  device.web_info.browser AS device_browser,
  device.web_info.hostname AS host_name,
  geo.country AS country, geo.city AS city, geo.region AS region,

  -- 거래 요약 / 날짜 (Asia/Seoul)
  ecommerce.transaction_id AS ecommerce_transaction_id,
  ecommerce.purchase_revenue AS purchase_revenue,
  DATE(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul')     AS date,
  DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS date_time,

  -- 유입 (cross_channel 우선, (not set) 정규화)
  CASE WHEN session_traffic_source_last_click.cross_channel_campaign.source='(not set)'
       THEN '(direct)' ELSE session_traffic_source_last_click.cross_channel_campaign.source END AS cross_channel_source,
  CASE WHEN session_traffic_source_last_click.cross_channel_campaign.medium='(not set)'
       THEN '(none)'   ELSE session_traffic_source_last_click.cross_channel_campaign.medium END AS cross_channel_medium,
  session_traffic_source_last_click.cross_channel_campaign.campaign_name         AS cross_channel_campaign,
  session_traffic_source_last_click.cross_channel_campaign.primary_channel_group AS channel_group,
  session_traffic_source_last_click.manual_campaign.source        AS manual_source,
  session_traffic_source_last_click.manual_campaign.medium        AS manual_medium,
  session_traffic_source_last_click.manual_campaign.campaign_name AS manual_campaign,
  session_traffic_source_last_click.manual_campaign.content       AS manual_content,
  session_traffic_source_last_click.manual_campaign.term          AS manual_term,

  -- 페이지
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key='page_title')    AS page_title,
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key='page_location') AS page_url,

  -- GA4 표준 상품 필드
  item.item_id, item.item_name, item.item_brand,
  item.item_category, item.item_category2, item.item_category3,
  item.item_category4, item.item_category5, item.item_variant,
  item.price, item.quantity, item.item_revenue, item.currency,
  item.item_list_id, item.item_list_name, item.item_list_index AS item_index,
  item.promotion_id, item.promotion_name, item.creative_slot, item.location_id,

  -- 커스텀 (예시)
  (SELECT value.double_value FROM UNNEST(item.item_params) WHERE key='item_discount_rate') AS item_discount_rate,
  (SELECT value.string_value FROM UNNEST(item.item_params) WHERE key='item_stock_status')  AS item_stock_status
FROM `[PROJECT_ID].analytics_[PROPERTY_ID].events_*`,
     UNNEST(items) AS item
WHERE _table_suffix = FORMAT_DATE('%Y%m%d', DATE_SUB(@run_date, INTERVAL 1 DAY))
  AND event_name IN (
    'view_item', 'view_item_list', 'select_item', 'add_to_cart', 'remove_from_cart',
    'view_cart', 'begin_checkout', 'add_shipping_info', 'add_payment_info',
    'purchase', 'refund', 'add_to_wishlist', 'select_promotion', 'view_promotion'
  );
```

### ③ 백필(과거 구간 한 번에)

SELECT 본문은 예약쿼리와 동일하고, 날짜 조건만 ‘하루’에서 ‘구간’으로 바꿉니다. 재실행 멱등성을 위해 그 구간을 먼저 지웁니다.

**Item_Performance.backfill.sql**
```sql
-- 1) 대상 구간 삭제
DELETE FROM `[PROJECT_ID].[DATASET_ID].Item_Performance`
WHERE date BETWEEN '2026-01-01' AND '2026-05-31';

-- 2) 같은 SELECT 본문으로 구간을 한 번에 적재
INSERT INTO `[PROJECT_ID].[DATASET_ID].Item_Performance`
SELECT
  /* ↑ '② 예약쿼리'와 완전히 동일한 SELECT 본문 + UNNEST(items) */
  ...
FROM `[PROJECT_ID].analytics_[PROPERTY_ID].events_*`,
     UNNEST(items) AS item
WHERE _table_suffix BETWEEN '20260101' AND '20260531'
  AND event_name IN ('view_item', 'add_to_cart', 'purchase', /* … */ 'view_promotion');
```

> **주의**
>
> **이커머스 이벤트만 거르는 게 중요합니다.** `page_view` 같은 비이커머스 이벤트엔 `items`가 비어 있어, 필터 없이 `UNNEST(items)`를 하면 그 행들은 그냥 사라지지만 쿼리는 쓸데없이 전체를 훑습니다. 이벤트 필터로 처리량과 의미를 함께 좁힙니다.

## Item_Performance로 답하는 질문들

**어제 매출 상위 10개 상품**
```sql
SELECT item_name, SUM(item_revenue) AS revenue, SUM(quantity) AS qty
FROM `[PROJECT_ID].[DATASET_ID].Item_Performance`
WHERE date = DATE_SUB(CURRENT_DATE('Asia/Seoul'), INTERVAL 1 DAY)
  AND event_name = 'purchase'
GROUP BY item_name
ORDER BY revenue DESC
LIMIT 10;
```

**상품별 조회 → 장바구니 전환(최근 7일)**
```sql
SELECT
  item_name,
  COUNTIF(event_name = 'view_item')  AS views,
  COUNTIF(event_name = 'add_to_cart') AS add_to_carts,
  SAFE_DIVIDE(COUNTIF(event_name = 'add_to_cart'), COUNTIF(event_name = 'view_item')) AS cart_rate
FROM `[PROJECT_ID].[DATASET_ID].Item_Performance`
WHERE date >= DATE_SUB(CURRENT_DATE('Asia/Seoul'), INTERVAL 7 DAY)
GROUP BY item_name
ORDER BY views DESC;
```

상품 단위로 펼쳐 둔 덕에 ‘어떤 상품이 많이 보였고, 얼마나 담겼고, 얼마 팔렸나’를 `UNNEST` 없이 한 번에 셉니다. 이벤트·세션 단위 질문은 [Event_Flat](/wiki/playbook/bigquery-checklist/what-is-event-flat-table)로 가면 됩니다.

## 자주 묻는 질문

### purchase_revenue와 item_revenue를 더하면 같아야 하지 않나요?

보통 한 거래의 `item_revenue` 합은 `purchase_revenue`와 가깝지만, 배송비·할인· 세금 처리에 따라 정확히 같지 않을 수 있습니다. **거래 총액은 `purchase_revenue`(이벤트 단위), 상품별 기여는 `item_revenue`(상품 단위)**로 구분해 쓰는 게 안전합니다.

### 구매 건수는 어떻게 세나요?

Item_Performance는 한 구매가 여러 행이라, 건수는 `COUNT(DISTINCT ecommerce_transaction_id)`로 셉니다. 행 수를 그대로 세면 ‘상품 라인 수’가 되어 구매 건수보다 큽니다.

### 왜 이커머스 이벤트만 담나요?

`items` 배열은 `view_item`·`add_to_cart`·`purchase` 같은 이커머스 이벤트에만 채워집니다. 그 외 이벤트는 `items`가 비어 있어 상품 마트에 들어갈 내용이 없으므로, 변환 쿼리에서 이커머스 이벤트만 거릅니다.

### 커스텀 상품 값(item_params)은 어떻게 더하나요?

상품에 붙는 커스텀 값은 `item.item_params` 안에 들어옵니다. `(SELECT value.string_value FROM UNNEST(item.item_params) WHERE key='...')` 패턴으로 꺼내 컬럼을 더하고(값 타입에 맞춰 `double_value`·`int_value` 사용), 스키마·정의서에 함께 반영합니다.

### Event_Flat과 Item_Performance를 합쳐 보려면요?

두 표는 단위가 달라 그냥 합치면 안 됩니다. 필요하면 `ecommerce_transaction_id`나 `session_id` 같은 공통 키로 **조인**하되, 양쪽을 미리 적절한 단위로 집계한 뒤 붙여야 중복이 생기지 않습니다.

## 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) (현재 문서)
