Blog
pysparksparkpivotwide-tablereshapedata-engineering

PySpark 대규모 피벗·와이드 변환 — 수천 컬럼으로 펼치기

고카디널리티 피벗이 느리거나 터지는 이유와 해법. pivot 의 2단계 비용, values 명시로 단일 패스 만들기, 수천 컬럼 와이드 테이블의 함정, map/array 대안, 그리고 unpivot(stack) 으로 되돌리는 패턴까지 정리합니다.

Data Dynamics2026年6月5日8 min read
This post is not yet translated. The original Korean version is shown below.

"상품별 매출을 월별 컬럼으로 펼쳐라", "사용자별로 이벤트 타입을 컬럼으로 만들어라" — 이런 피벗(pivot) 요청은 분석에서 흔하지만, 카디널리티가 크면 PySpark 에서 느려지거나 터집니다. 피벗 대상 값이 수천 개면 컬럼이 수천 개인 와이드 테이블이 되고, Spark 는 이걸 잘 다루지 못합니다.

이 글은 피벗이 왜 비싼지, values 명시로 비용을 절반으로 줄이는 법, 와이드 테이블의 함정, 그리고 map/array 대안과 unpivot 패턴을 정리합니다.

1. 기본 피벗과 숨은 비용

from pyspark.sql import functions as F
 
# 상품 × 월 피벗
pivoted = (df
    .groupBy("product")
    .pivot("month")               # month 값들이 컬럼이 됨
    .agg(F.sum("amount")))

여기엔 숨은 비용이 있습니다. Spark 는 어떤 컬럼을 만들지 모릅니다. month 에 어떤 고유값이 있는지 알아야 컬럼을 정할 수 있으므로, 피벗은 내부적으로 두 번 스캔합니다.

1차 스캔: pivot 컬럼의 고유값 수집 (distinct month)
2차 스캔: 실제 집계
→ 데이터를 두 번 읽음

2. 해법 ① values 명시 — 단일 패스로

피벗할 값을 미리 알고 있다면 명시하세요. 1차 distinct 스캔이 사라져 비용이 절반이 됩니다.

months = ["2026-01", "2026-02", "2026-03", "2026-04",
          "2026-05", "2026-06"]
 
pivoted = (df
    .groupBy("product")
    .pivot("month", months)       # 값 명시 → 단일 패스
    .agg(F.sum("amount")))
values 미지정values 명시
스캔2회(distinct + 집계)1회
컬럼 결정런타임 추론고정
속도느림빠름

실무 규칙: 피벗 대상 값이 알려진 유한 집합(월, 카테고리, 상태값)이면 항상 명시하세요. 모르면 별도로 distinct 를 구한 뒤 명시하는 게, pivot 에 맡기는 것보다 제어가 쉽습니다.

3. 해법 ② 고카디널리티 피벗을 막기

피벗 대상 카디널리티에 상한이 있습니다. Spark 는 기본적으로 피벗 컬럼이 너무 많으면(기본 1만) 거부합니다 — 이건 보호 장치입니다.

pivot 컬럼 수가 수천~수만 → 와이드 테이블
→ 행마다 수천 컬럼, 대부분 NULL(희소) → 메모리·성능 재앙
# 한계를 늘릴 수는 있지만 (보통 나쁜 신호)
spark.conf.set("spark.sql.pivotMaxValues", "10000")

피벗 카디널리티가 크면, 피벗이 잘못된 도구라는 신호입니다. 다음 대안을 고려하세요.

4. 해법 ③ map / array — 와이드 대신 구조화

수천 개의 키-값을 굳이 컬럼으로 펼치지 말고, 하나의 map 컬럼에 담으면 희소 데이터를 효율적으로 표현합니다.

# 피벗(와이드) 대신 map 으로 집계
as_map = (df
    .groupBy("product")
    .agg(F.map_from_entries(
        F.collect_list(F.struct("month", "amount"))).alias("by_month")))
 
# 조회는 map 접근
as_map.select("product", F.col("by_month")["2026-06"].alias("jun"))
표현적합
피벗(와이드 컬럼)카디널리티 작음(수십), BI 친화
map 컬럼카디널리티 큼, 희소, 동적 키
long format(그대로)집계·필터가 주목적

핵심 통찰: "컬럼으로 펼쳐야 한다"는 요구는 대개 표현(presentation) 단계의 필요입니다. 처리·저장은 long format 이나 map 으로 두고, 최종 표시 직전에만 작은 범위로 피벗하는 것이 효율적입니다.

5. 와이드 테이블의 함정

수백~수천 컬럼 테이블 자체가 Spark 에 부담입니다.

함정이유
플래너 지연컬럼 수에 비례해 계획 수립 느려짐
코드젠 한계Whole-Stage CodeGen 이 컬럼 많으면 비효율/폴백
희소·NULL 낭비대부분 NULL 인 컬럼 저장·처리 비용
직렬화 비용행마다 수천 필드

수천 컬럼이 정말 필요한지 자문하세요. 분석·ML 입력이라면 보통 필요한 피처만 선택하거나 벡터로 묶는 게 낫습니다.

6. Unpivot — 와이드를 다시 long 으로

반대로 와이드 테이블(컬럼이 값인)을 long format 으로 되돌려야 할 때가 많습니다(정규화, 집계 용이). stack 또는 Spark 3.4+ 의 unpivot 을 씁니다.

# Spark 3.4+ unpivot (melt)
long_df = wide_df.unpivot(
    ids=["product"],
    values=["jan", "feb", "mar"],
    variableColumnName="month",
    valueColumnName="amount")
 
# 또는 stack 표현식 (구버전)
long_df = wide_df.select(
    "product",
    F.expr("stack(3, 'jan', jan, 'feb', feb, 'mar', mar) as (month, amount)"))

long format 은 집계·필터·조인에 훨씬 유리합니다. "저장·처리는 long, 표시만 wide" 원칙의 실천 도구입니다.

7. 다중 집계 피벗

여러 지표를 동시에 피벗하면 컬럼이 곱으로 늘어납니다(값 × 지표).

pivoted = (df
    .groupBy("product")
    .pivot("month", months)
    .agg(
        F.sum("amount").alias("sum"),
        F.count("*").alias("cnt")))
# → month당 sum, cnt 두 컬럼 → 컬럼 수 2배

지표가 늘수록 와이드 폭발이 가속되므로, 다중 집계 피벗은 카디널리티를 더 보수적으로 잡으세요.

8. 정리

해법핵심
values 명시2스캔 → 1스캔
카디널리티 상한 인지수천 컬럼은 잘못된 신호
map/array희소·동적 키를 구조화
long format 유지처리·저장은 long, 표시만 wide
unpivot/stack와이드 → long 복원

대규모 피벗의 핵심은 "정말 컬럼으로 펼쳐야 하는가"를 먼저 묻는 것입니다. 피벗 대상이 작고 알려진 집합이면 values 를 명시해 단일 패스로 처리하고, 카디널리티가 크면 피벗 대신 map/long format 으로 두었다가 표시 직전에만 좁게 펼치세요. 수천 컬럼 와이드 테이블은 Spark 가 가장 싫어하는 형태라는 점만 기억하면, 피벗은 더 이상 잡을 터뜨리는 연산이 아닙니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. 대규모 데이터 reshape·집계 파이프라인 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀