티스토리 뷰
목차
실무 데이터베이스 환경에서 데이터 분석이나 경영진 보고서(BI, Business Intelligence)용 대시보드를 구축하다 보면, 단순히 특정 기준에 따라 데이터를 그룹화하여 합계나 평균을 구하는 기초적인 수준을 넘어, 부서별 소계, 지역별 중분류 합계, 그리고 전체 총합계(Grand Total)까지 한 번의 쿼리로 동시에 추출해야 하는 복잡한 다차원 집계 요구사항에 직면하게 됩니다. 과거에는 이러한 다중 수준의 집계 결과를 얻기 위해 동일한 테이블을 여러 번 스캔하고 UNION ALL 연산자로 무식하게 이어 붙이는 비효율적인 하드코딩 방식을 사용했습니다. 하지만 이는 극심한 시스템 디스크 I/O 부하를 유발하고 SQL 코드의 가독성을 심각하게 훼손하는 치명적인 단점이 있었습니다. 이러한 구조적 한계를 극복하고 데이터 집계 연산의 패러다임을 혁신하기 위해 ANSI SQL-1999 표준에서 공식적으로 도입된 강력한 무기가 바로 확장 그룹 함수(Extended Grouping Functions)입니다. 오늘 포스팅에서는 관계형 데이터베이스(RDBMS) 집계 연산의 3대장이라고 불리는 ROLLUP, CUBE, GROUPING SETS 함수의 각기 다른 탄생 배경과 내부적인 작동 원리, 장단점, 그리고 실무에서 어떤 비즈니스 로직에 맞추어 전략적으로 취사선택해야 하는지를 현업 DBA와 데이터 엔지니어의 관점에서 아주 깊이 있고 완벽하게 비교 분석해 드리겠습니다.
1. 일반 GROUP BY의 한계와 ROLLUP 함수의 등장 (계층적 소계의 달인)
전통적인 GROUP BY 절은 명시된 컬럼들의 '고유한 조합'에 대해서만 단일 레벨의 집계 결과를 반환합니다. 예를 들어 '연도', '월', '부서' 기준으로 그룹화를 하면 오직 그 세 가지 기준이 모두 일치하는 최하위 레벨의 교집합 데이터만 도출될 뿐, '연도별 총합'이나 '연도-월별 소계'와 같은 중간 단계의 합계는 절대 자동으로 계산해주지 않습니다. 이럴 때 가장 먼저 고려해야 하는 강력한 해결책이 바로 ROLLUP 함수입니다. ROLLUP은 영어 단어의 의미처럼 데이터를 아래에서부터 위로 '말아 올리면서' 단계적인 소계(Subtotal)와 최종적인 총계(Grand Total)를 생성해 내는 데 특화되어 있는 계층형 집계 함수입니다. 데이터베이스 엔진은 ROLLUP 괄호 안에 명시된 인자(컬럼)들의 순서를 매우 중요하게 인식하며, 오른쪽에서 왼쪽 방향으로 하나씩 기준을 제거해 나가는 방식으로 순차적인 집계 레벨을 형성합니다.
작동 원리를 수학적으로 분해해보면, 만약 GROUP BY ROLLUP(A, B, C)라고 쿼리를 작성할 경우, 데이터베이스 내부에서는 총 4가지 레벨의 그룹화가 발생합니다. 첫 번째로 명시된 모든 컬럼(A, B, C)을 기준으로 한 기초 집계가 수행되고, 그다음 맨 오른쪽의 C를 버리고 (A, B) 기준으로 소계를 구합니다. 이어서 B마저 버리고 (A) 기준으로 중분류 소계를 구하며, 마지막으로는 모든 컬럼을 무시하고 전체 테이블 데이터에 대한 () 즉, 총합계를 계산하여 결과 집합에 추가합니다. 인자의 개수가 N개라면 정확히 N+1개의 집계 레벨이 도출되는 방식입니다. 이러한 ROLLUP의 단계적이고 방향성 있는 논리적 특성 때문에, 연도-월-일과 같은 명확한 시간적 계층 구조나 본부-부서-팀과 같은 수직적인 조직도 데이터, 혹은 대분류-중분류-소분류로 이어지는 상품 카테고리의 매출 합계를 산출할 때 가장 완벽하고 이상적인 성능을 발휘하게 됩니다. 만약 괄호 안의 컬럼 순서를 (C, B, A)로 무심코 뒤바꾸어 작성한다면 비즈니스 로직상 완전히 무의미하고 엉뚱한 기준의 소계가 발생하므로, 반드시 상위 계층에서 하위 계층 순으로 나열해야 한다는 점을 실무에서 각별히 유의해야 합니다.





2. CUBE 함수: 가능한 모든 경우의 수를 추출하는 다차원 결합의 끝판왕
ROLLUP이 정해진 계층 순서에 따라 단계적으로 말아 올리는 선형적인 집계를 수행한다면, CUBE 함수는 이름(정육면체)이 암시하듯 인자로 주어진 컬럼들로 생성할 수 있는 '모든 수학적 경우의 수'를 다차원(Multi-Dimensional) 공간에서 전부 조합하여 맹렬하게 집계해 내는 파괴력을 가진 함수입니다. CUBE 연산은 방향성이나 계층적 종속성을 완전히 무시하며, 오로지 입력된 컬럼들을 이용해 가능한 모든 부분 집합(Power Set)에 대한 크로스탭(Cross-Tabulation) 결과를 생성해 냅니다. 예를 들어, 성별(남/여)과 지역(서울/부산), 연령대(20/30대)라는 서로 종속 관계가 전혀 없는 독립적인 변수들의 모든 교차 분석 데이터가 필요할 때 CUBE 함수는 그 진가를 발휘합니다.
내부적인 연산 방식을 살펴보면, GROUP BY CUBE(A, B, C)로 작성된 쿼리는 2의 3승, 즉 무려 8가지의 완전한 조합 집합을 뱉어냅니다. 전체 총합인 ()를 시작으로 개별 컬럼 기준인 (A), (B), (C), 두 컬럼 조합인 (A, B), (A, C), (B, C), 그리고 전체 조합인 (A, B, C)에 대한 모든 서브토탈이 단 한 번의 쿼리로 쏟아져 나옵니다. 이렇게 입체적인 다차원 분석 데이터를 손쉽게 얻을 수 있다는 것은 OLAP(온라인 분석 처리) 환경에서 엄청난 장점이지만, 양날의 검처럼 무시무시한 성능 저하의 리스크를 동시에 내포하고 있습니다. 데이터베이스 엔진은 이 모든 조합을 연산하기 위해 내부적으로 막대한 양의 메모리를 할당하여 다중 정렬(Multi-Sort)과 해시(Hash) 연산을 동시에 수행해야 합니다. 만약 CUBE 괄호 안에 들어가는 컬럼의 수가 5개라면 2의 5승인 32개의 뷰가 생성되고, 10개라면 1,024개의 조합이 폭발적으로 생성되는 카테시안 곱(Cartesian Product)과 유사한 부하가 발생하게 됩니다. 따라서 대용량 트랜잭션 테이블을 대상으로 아무 생각 없이 CUBE 함수를 남용할 경우, 임시 테이블스페이스의 고갈과 함께 데이터베이스 전체의 락(Lock)을 유발하거나 시스템 장애를 초래할 수 있으므로, 현업 DBA들은 CUBE의 사용을 인덱스가 잘 설계된 작은 규모의 데이터 마트(Data Mart)나 집계가 1차적으로 완료된 서브쿼리 결과에 대해서만 제한적으로 허용하는 것을 강력히 권고합니다.
3. GROUPING SETS 함수: 필요한 부분집합만 정밀 타격하는 실용성의 제왕
ROLLUP이 강제적인 계층 구조에 얽매여 있고, CUBE가 지나치게 방대한 모든 경우의 수를 무차별적으로 쏟아내어 성능상 부담스럽다면, 이 두 함수의 단점을 완벽하게 보완하고 개발자에게 극도의 유연성을 부여하는 가장 세련된 도구가 바로 GROUPING SETS 함수입니다. GROUPING SETS는 데이터베이스 옵티마이저에게 "내가 괄호 안에 명시적으로 지정한 정확히 그 조합들에 대해서만 집계 연산을 수행해 달라"고 명령하는 스나이퍼와 같은 함수입니다. 불필요한 연산 과정을 원천적으로 차단하기 때문에, 확장 그룹 함수 3대장 중에서 가장 가볍고 디테일한 성능 튜닝이 가능한 핵심 기능으로 평가받고 있습니다.
예를 들어 비즈니스 요구사항이 "전체 부서별 합계"와 "전체 직급별 합계" 딱 두 가지의 요약 정보만 필요로 한다고 가정해 보겠습니다. 이를 위해 ROLLUP(부서, 직급)을 사용하면 원치 않는 최하위 (부서, 직급) 교집합과 전체 총합계()까지 4단계가 강제 계산되고, CUBE(부서, 직급)를 쓰면 역시나 쓸데없는 조합들이 튀어나와 시스템 리소스만 갉아먹게 됩니다. 하지만 GROUPING SETS((부서), (직급))이라고 명시하면, 데이터베이스는 군더더기 없이 부서를 기준으로 한 독립적인 그룹화 연산과 직급을 기준으로 한 독립적인 그룹화 연산 딱 두 가지만 수행한 뒤 결과를 깔끔하게 병합하여 반환합니다. 이는 본질적으로 SELECT ... GROUP BY 부서 UNION ALL SELECT ... GROUP BY 직급 이라고 작성하는 긴 하드코딩 쿼리와 논리적으로 100% 동일한 결과를 내면서도, 내부적으로는 테이블 풀 스캔(Full Table Scan)을 단 한 번만 수행하도록 옵티마이저 경로를 최적화해주므로 쿼리 실행 시간을 절반 이하로 획기적으로 단축시키는 마법을 부립니다. 또한 괄호 안에 전체 총합계를 의미하는 빈 괄호 '()'를 임의로 끼워 넣을 수도 있어, 보고서 출력물의 형태를 개발자의 입맛대로 아주 미세하게 컨트롤할 수 있는 극강의 실용성을 자랑합니다.





4. 혼동 주의! 복합 그룹화(Composite Grouping) 기법과 부분 롤업
실무 현장의 복잡한 비즈니스 로직을 SQL로 완벽하게 구현하기 위해서는 단일 그룹 함수를 넘어서, 여러 컬럼을 하나로 묶어 단위 연산을 수행하는 '복합 그룹화(Composite Grouping)' 스킬을 반드시 숙지해야 합니다. 확장 그룹 함수의 괄호 안에서 여러 개의 컬럼을 다시 추가적인 소괄호 '( )'로 묶어주게 되면, 데이터베이스 엔진은 해당 컬럼들을 각각 분리된 개체가 아닌 마치 하나의 거대한 단일 컬럼 덩어리처럼 취급하여 그룹핑 연산을 수행합니다. 이 미세한 괄호의 차이는 실행 계획과 출력되는 행의 건수에 엄청난 나비효과를 불러일으킵니다.
대표적인 예로 ROLLUP 연산을 살펴보겠습니다. 만약 GROUP BY ROLLUP(연도, 월, 일)이라고 작성하면 총합(), (연도), (연도, 월), (연도, 월, 일)의 총 4가지 계층 레벨이 생성됩니다. 하지만 비즈니스 요구사항에서 굳이 '연도' 단위의 단독 소계는 필요 없고 '연도와 월이 결합된 달(Month)' 단위의 소계만 필요하다면 어떻게 해야 할까요? 바로 다중 괄호를 사용하여 GROUP BY ROLLUP((연도, 월), 일) 형태로 작성하면 됩니다. 이렇게 지정하면 엔진은 (연도, 월)을 쪼갤 수 없는 하나의 단위로 인식하기 때문에, 최종 결과는 전체 총합계(), 그리고 (연도, 월) 소계, (연도, 월, 일) 기초 집계까지 딱 3단계의 필수적인 결과 집합만을 간결하게 생성하게 됩니다. 마찬가지로 CUBE((A, B), C) 형식으로 작성하면 A와 B는 절대 떨어지지 않고 항상 붙어 다니며 크로스탭을 형성하므로, 불필요한 조합 생성을 사전에 차단하여 CUBE 연산의 치명적인 성능 저하 단점을 상당히 상쇄시킬 수 있습니다. 이러한 기법을 부분 롤업(Partial Rollup) 또는 부분 큐브(Partial Cube)라고 부르며, 현업에서 대용량 데이터를 다루는 DBA들이 시스템 자원을 극한으로 아끼면서 원하는 대시보드 데이터를 추출할 때 가장 빈번하게 구사하는 고급 쿼리 작성 테크닉입니다. 이처럼 괄호의 배치 하나로 수십만 건의 중간 집계 연산을 스킵할 수 있다는 점을 명심하고 쿼리 구조를 입체적으로 설계해야 합니다.
5. 가독성과 무결성을 위한 GROUPING 함수의 활용 전략
ROLLUP이나 CUBE와 같은 다중 집계 함수를 사용하여 출력된 최종 결과를 화면이나 엑셀 보고서에서 살펴보면, 소계나 총계가 산출된 행의 기준 컬럼 자리에는 항상 텅 빈 'NULL' 값이 채워져 있는 것을 발견할 수 있습니다. 이는 "해당 컬럼의 조건을 무시하고 전체를 통합하여 계산했다"라는 데이터베이스 엔진만의 암묵적인 표시입니다. 하지만 여기서 매우 심각하고 골치 아픈 논리적 모순이 발생합니다. 만약 원본 테이블의 해당 컬럼 데이터 자체에 진짜로 값이 비어있는 오리지널 NULL 데이터가 존재했다면 어떻게 될까요? 화면에 출력된 NULL이 원본 데이터가 결측되어 발생한 진짜 NULL인지, 아니면 ROLLUP 집계 연산 과정에서 데이터베이스가 인위적으로 만들어낸 껍데기 가짜 NULL(Super-aggregate NULL)인지 육안으로는 절대 구별할 수 없는 치명적인 정보의 혼동 사태가 벌어집니다. 이러한 논리적 충돌을 완벽하게 해결하고 보고서의 무결성을 유지하기 위해 짝꿍처럼 반드시 함께 사용해야 하는 보조 함수가 바로 'GROUPING' 함수와 확장형인 'GROUPING_ID' 함수입니다.
GROUPING(컬럼명) 함수는 인자로 전달된 컬럼이 집계 작업에 참가를 했는지, 아니면 배제되어 소계용 NULL로 변환되었는지를 검사하여 아주 직관적인 숫자를 반환합니다. 만약 해당 행이 일반적인 데이터 조회 결과이거나 진짜 NULL 데이터여서 집계에 참가했다면 숫자 '0'을 뱉어내고, 반대로 집계 연산에 의해 컬럼이 제외되면서 인위적으로 생성된 소계용 행의 가짜 NULL이라면 숫자 '1'을 반환합니다. 이 절대적인 성질을 이용하면 실무에서 보고서의 포맷팅을 완벽하게 제어할 수 있습니다. SELECT 절에 CASE 구문이나 DECODE, NVL 등의 분기 함수를 조합하여, "만약 GROUPING(부서) 결과가 1이라면 화면에 NULL 대신 '부서별 소계'라는 텍스트를 강제로 출력하라"는 식의 동적 라벨링(Dynamic Labeling) 처리가 가능해집니다. 한 발 더 나아가 오라클 등에서 제공하는 GROUPING_ID(컬럼1, 컬럼2) 함수를 사용하면, 각 컬럼의 GROUPING 반환값(0 또는 1)을 이진수 비트(Bitwise) 형태로 결합하여 하나의 십진수 정수로 표현해주므로, 컬럼이 여러 개 얽혀 있는 복잡한 CUBE 환경에서도 이 데이터가 정확히 어느 레벨의 소계 집합인지 코드로 단번에 식별할 수 있는 강력한 무결성 제어 능력을 확보하게 됩니다. 따라서 훌륭한 SQL 개발자라면 확장 그룹 함수를 작성할 때 무조건 GROUPING 함수를 세트로 묶어서 예외 처리를 해두는 방어적 코딩 습관을 반드시 길러야만 합니다.






