Blog
pysparksparkpivotwide-tablereshapedata-engineering

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

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

Data Dynamics2026년 6월 5일8 min read

"상품별 매출을 월별 컬럼으로 펼쳐라", "사용자별로 이벤트 타입을 컬럼으로 만들어라" — 이런 피벗(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 엔지니어링 팀