PySpark 다차원 집계 — rollup, cube, grouping sets 마스터
소계·총계·다차원 교차 집계를 한 번의 잡으로 만드는 법. rollup(계층 소계), cube(모든 조합), grouping sets(원하는 조합만)의 차이와 비용, grouping_id 로 집계 레벨을 구분하는 법, 그리고 고카디널리티 cube 의 폭발을 피하는 패턴을 정리합니다.
"국가별, 국가×상품별, 그리고 전체 합계를 한 번에 보여줘." 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) ← 총계| rollup | cube | |
|---|---|---|
| 조합 | 계층적(왼쪽부터) | 모든 부분집합 |
| 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/cube | long(행으로 레벨 표현) |
| pivot | wide(컬럼으로 펼침) |
다차원 집계는 결과를 행으로 만들고(레벨 컬럼으로 구분), 피벗은 컬럼으로 펼칩니다(별도 글 "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 엔지니어링 팀