Blog
pysparksparkaggregationrollupcubedata-engineering

PySpark 다차원 집계 — rollup, cube, grouping sets 마스터

소계·총계·다차원 교차 집계를 한 번의 잡으로 만드는 법. rollup(계층 소계), cube(모든 조합), grouping sets(원하는 조합만)의 차이와 비용, grouping_id 로 집계 레벨을 구분하는 법, 그리고 고카디널리티 cube 의 폭발을 피하는 패턴을 정리합니다.

Data Dynamics2026년 6월 5일8 min read

"국가별, 국가×상품별, 그리고 전체 합계를 한 번에 보여줘." OLAP 리포트의 단골 요구입니다. 순진하게 구현하면 groupBy 를 여러 번 돌리고 union 으로 합치는 지저분한 코드가 됩니다. PySpark 는 rollup, cube, grouping sets 로 이 다차원 집계를 한 번의 잡으로 우아하게 처리합니다.

이 글은 세 가지 다차원 집계의 차이와 비용, 집계 레벨을 구분하는 법, 그리고 고카디널리티에서 cube 가 폭발하지 않게 다루는 법을 정리합니다.

1. 문제 — 여러 수준의 집계를 한 번에

원하는 결과:
  (국가, 상품) 별 매출   ← 가장 세밀
  (국가)      별 매출    ← 상품 합친 소계
  전체 합계              ← 총계

이걸 groupBy("country","product"), groupBy("country"), 전체 집계를 따로 돌려 union 하면 — 데이터를 3번 스캔하고 코드가 장황해집니다. 다차원 집계 연산이 이를 한 번의 스캔으로 해결합니다.

2. rollup — 계층적 소계

rollup왼쪽부터 계층적으로 소계를 만듭니다. 컬럼 순서가 의미를 갖습니다.

from pyspark.sql import functions as F
 
result = (df
    .rollup("country", "product")
    .agg(F.sum("amount").alias("total"))
    .orderBy("country", "product"))

생성되는 집계 레벨:

rollup(country, product) 는 다음을 만든다:
  (country, product)   ← 세밀
  (country, NULL)      ← 국가별 소계 (product 합침)
  (NULL,    NULL)      ← 총계

rollup 은 "계층 구조"에 맞습니다 — 연→월→일, 국가→도시, 카테고리→상품처럼 상위가 하위를 포함하는 관계. 컬럼 순서가 계층 순서입니다.

3. cube — 모든 조합

cube 는 컬럼들의 가능한 모든 조합을 만듭니다.

result = df.cube("country", "product").agg(F.sum("amount").alias("total"))
cube(country, product) 는 다음을 만든다:
  (country, product)   ← 둘 다
  (country, NULL)      ← country 만
  (NULL,    product)   ← product 만   ← rollup 엔 없는 조합!
  (NULL,    NULL)      ← 총계
rollupcube
조합계층적(왼쪽부터)모든 부분집합
n개 컬럼n+1 레벨2^n 레벨
적합계층 소계다차원 교차분석

주의: cube 는 컬럼이 늘수록 조합이 2^n 으로 폭증합니다. 4개 컬럼이면 16가지 집계. 고카디널리티 컬럼이 섞이면 결과 행이 폭발하니, 정말 모든 조합이 필요한지 확인하세요.

4. grouping sets — 원하는 조합만

rollup 도 cube 도 아닌 특정 조합만 원하면 grouping sets 를 씁니다(SQL 로 가장 명확).

df.createOrReplaceTempView("sales")
 
result = spark.sql("""
    SELECT country, product, sum(amount) AS total
    FROM sales
    GROUP BY GROUPING SETS (
        (country, product),   -- 세밀
        (country),            -- 국가별
        ()                    -- 총계
        -- (product) 는 일부러 제외
    )
""")

grouping sets 는 필요한 집계 레벨만 골라서 계산하므로, cube 의 불필요한 조합 비용을 피합니다. "이 조합과 저 조합만" 같은 맞춤 리포트에 최적입니다.

5. 집계 레벨 구분 — grouping_id / grouping

결과에서 NULL 이 "소계라서 NULL"인지 "원래 데이터가 NULL"인지 구분해야 합니다. grouping_id()grouping() 이 이를 알려줍니다.

result = (df
    .rollup("country", "product")
    .agg(
        F.sum("amount").alias("total"),
        F.grouping_id().alias("gid"),                 # 집계 레벨 비트마스크
        F.grouping("country").alias("is_country_agg"))  # 1이면 이 컬럼이 합쳐짐
)
 
# gid 로 레벨 해석
result = result.withColumn("level",
    F.when(F.col("gid") == 0, "country+product")
     .when(F.col("gid") == 1, "country subtotal")
     .when(F.col("gid") == 3, "grand total"))
함수의미
grouping(col)그 컬럼이 집계로 합쳐졌으면 1, 아니면 0
grouping_id()모든 컬럼의 grouping 비트를 합친 정수

실무 필수: 소계/총계 행의 NULL 과 데이터의 진짜 NULL 을 구분하려면 grouping_id 가 반드시 필요합니다. 이걸 안 쓰면 "NULL 국가"가 총계인지 결측인지 알 수 없습니다.

6. 고카디널리티 폭발 막기

다차원 집계의 결과 행 수는 (각 차원 카디널리티의 조합)입니다. 고카디널리티 컬럼을 cube 에 넣으면 결과가 거대해집니다.

cube(country[200], product[100000], date[365])
→ 최악의 경우 조합이 수십억 행 💥

대응:

전략방법
grouping sets 로 한정필요한 레벨만 명시
고카디널리티 제외세밀 차원은 cube 에서 빼기
사전 필터·집계차원 카디널리티 축소 후
결과를 롤업 테이블로미리 계산해 저장(별도 글 "고카디널리티 집계")

cube 대신 grouping sets 로 실제 필요한 조합만 지정하는 것이 가장 안전합니다.

7. 다차원 집계 vs 피벗

도구결과 형태
rollup/cubelong(행으로 레벨 표현)
pivotwide(컬럼으로 펼침)

다차원 집계는 결과를 으로 만들고(레벨 컬럼으로 구분), 피벗은 컬럼으로 펼칩니다(별도 글 "PySpark 대규모 피벗"). 저장·재집계엔 long 형태가, 표시엔 wide 가 유리합니다.

8. 정리

연산만드는 것적합
rollup계층적 소계(n+1)연→월→일 등 계층
cube모든 조합(2^n)다차원 교차분석
grouping sets지정한 조합만맞춤 리포트, 폭발 회피
grouping_id집계 레벨 구분소계 NULL vs 데이터 NULL

다차원 집계의 핵심은 "소계·총계·교차 집계를 한 번의 스캔으로" 만든다는 것입니다. 계층 소계는 rollup, 전방위 교차는 cube, 맞춤 조합은 grouping sets — 그리고 결과의 NULL 을 grouping_id 로 해석하면 됩니다. 단 하나, cube 의 2^n 폭발만 경계하세요. 고카디널리티 차원이 섞이면 grouping sets 로 필요한 레벨만 골라, OLAP 리포트를 효율적으로 생성할 수 있습니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. OLAP 리포트·다차원 집계 파이프라인 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀