Blog
pysparksparkpivotwide-tablereshapedata-engineering

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

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

Data Dynamics2026년 6월 5일12 min read

엑셀 스프레드시트를 상상해 보세요. 월별 매출을 열 하나씩 배치했을 때, 열이 12개라면 문제없습니다. 그런데 만약 상품 ID 5만 개가 각각 열 하나를 차지하게 된다면? 시트가 폭발합니다. PySpark 에서 피벗(pivot) 이 하는 일이 바로 이것입니다. "상품별 매출을 월별 컬럼으로 펼쳐라", "사용자별로 이벤트 타입을 컬럼으로 만들어라" — 분석에서 흔한 요청이지만, 카디널리티가 커지는 순간 Spark 잡이 느려지거나 아예 터집니다.

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

이 글에서 배우는 것

  • 피벗이 내부적으로 데이터를 두 번 스캔하는 이유
  • values 명시 한 줄로 스캔을 절반으로 줄이는 방법
  • 고카디널리티 피벗이 왜 위험하고, 어떤 대안이 있는지
  • 와이드 테이블이 Spark 에 주는 부담과 map/long 형식 대안
  • unpivot/stack 으로 와이드를 다시 long 으로 되돌리는 패턴

1. 기본 피벗과 숨은 비용

피벗은 써보면 간단해 보이지만, 뚜껑을 열면 생각보다 무거운 연산이 숨어 있습니다.

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

여기엔 숨은 비용이 있습니다. Spark 는 어떤 컬럼을 만들지 미리 모릅니다. month 에 어떤 고유값이 존재하는지 파악해야 컬럼 목록을 결정할 수 있으므로, 피벗은 내부적으로 두 번 스캔합니다.

Loading diagram…

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

가장 간단한 최적화입니다. 피벗할 값을 여러분이 이미 알고 있다면, Spark 에게 알려 주세요. 그러면 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. 해법 ② 고카디널리티 피벗을 막기

values 를 명시해도 피벗 자체가 잘못된 선택인 경우가 있습니다. 피벗 대상 고유값이 수천~수만 개라면, 컬럼 수가 그만큼 불어나기 때문입니다. Spark 는 기본적으로 피벗 컬럼이 너무 많으면(기본 1만) 아예 거부합니다 — 이건 여러분을 보호하는 장치입니다.

Loading diagram…
# 한계를 늘릴 수는 있지만 (보통 나쁜 신호)
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 입력이라면 대개 필요한 피처만 선택하거나 벡터로 묶는 편이 훨씬 낫습니다.

한 문장으로: 와이드 테이블은 Spark 가 가장 다루기 어려워하는 형태 중 하나입니다.

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

피벗의 역방향도 알아 두면 유용합니다. 외부에서 받아온 와이드 테이블을 long format 으로 되돌려야 정규화나 집계가 훨씬 쉬워지거든요. Spark 3.4+ 의 unpivot 또는 구버전의 stack 표현식을 쓰면 됩니다.

# 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배

지표가 늘수록 와이드 폭발이 가속됩니다. 다중 집계 피벗을 쓸 때는 카디널리티를 더욱 보수적으로 잡으세요.

⚠️ 지표 3개 × 고유값 1,000개 = 컬럼 3,000개. 생각보다 빠르게 터집니다.

마치며 — 핵심 요약

  • 피벗은 내부적으로 두 번 스캔합니다. values 를 명시하는 것만으로 스캔 횟수를 절반으로 줄일 수 있습니다.
  • 고카디널리티 피벗은 잘못된 도구를 선택했다는 신호입니다. 수천 컬럼 대신 map 컬럼이나 long format 을 고려하세요.
  • 와이드 테이블은 Spark 플래너, 코드젠, 직렬화 모두에 부담을 줍니다. 꼭 필요한 경우에만 작은 범위로 사용하세요.
  • "저장·처리는 long, 표시만 wide" 원칙을 기억하세요. 최종 출력 직전에만 좁게 피벗하면 됩니다.
  • 다중 집계 피벗은 컬럼이 값 × 지표 수로 폭발합니다. 카디널리티를 더 보수적으로 잡으세요.

피벗이 Spark 잡을 터뜨리는 경험을 하셨다면, 이 원칙들을 하나씩 적용해 보세요 — 대부분의 문제는 values 명시 하나로 시작됩니다.

8. 정리

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

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


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

— Data Dynamics 엔지니어링 팀