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/롱 포맷 대안
  • unpivot/stack 으로 와이드를 다시 롱 포맷으로 되돌리는 패턴

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 컬럼카디널리티 큼, 희소, 동적 키
롱 포맷(그대로)집계·필터가 주목적

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

5. 와이드 테이블의 함정

피벗을 써서 와이드 테이블을 만드는 데 성공했다 하더라도, 수백~수천 컬럼을 가진 테이블 자체가 Spark 에게 여러 형태의 부담을 줍니다. 이 함정들을 알고 있어야 합니다.

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

수천 컬럼이 정말 필요한지 스스로에게 물어보세요. 분석·ML 입력이라면 대개 필요한 피처만 선택하거나 벡터로 묶는 편이 훨씬 낫습니다.

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

6. Unpivot — 와이드를 다시 롱 포맷으로

피벗의 역방향도 알아 두면 유용합니다. 외부에서 받아온 와이드 테이블을 롱 포맷으로 되돌려야 정규화나 집계가 훨씬 쉬워지거든요. 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, 표시만 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 컬럼이나 롱 포맷을 고려하세요.
  • 와이드 테이블은 Spark 플래너, 코드젠, 직렬화 모두에 부담을 줍니다. 꼭 필요한 경우에만 작은 범위로 사용하세요.
  • "저장·처리는 long, 표시만 wide" 원칙을 기억하세요. 최종 출력 직전에만 좁게 피벗하면 됩니다.
  • 다중 집계 피벗은 컬럼이 값 × 지표 수로 급증합니다. 카디널리티를 더 보수적으로 잡으세요.

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

8. 정리

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

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


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

— Data Dynamics 엔지니어링 팀