SQL 윈도우 함수

데이터를 다루다 보면 개별 행의 값을 넘어, 전체 혹은 특정 그룹 내에서 각 행이 어떤 의미를 가지는지 파악해야 할 때가 많습니다.
예를 들어, "각 직원의 급여가 자신이 속한 부서의 평균 급여와 비교해서 어느 정도 수준일까?" 또는 "월별 매출 데이터를 기반으로 전월 대비 성장률을 계산하고 싶다"와 같은 질문들이죠.
과거에는 이런 분석을 위해 복잡한 서브쿼리나 여러 번의 조인이 필요했지만, SQL 윈도우 함수를 사용하면 이 문제를 훨씬 간결하고 효율적으로 해결할 수 있습니다.
SQL 윈도우 함수, 왜 필요할까요?
Section titled “SQL 윈도우 함수, 왜 필요할까요?”기존의 집계 함수(SUM(), AVG(), COUNT() 등)는 GROUP BY 절과 함께 사용되어 여러 행을 그룹화하고 각 그룹당 하나의 요약된 결과 값을 반환합니다. 이 과정에서 개별 행의 정보는 사라지죠.
하지만 윈도우 함수는 다릅니다. 결과 집합의 행 수를 그대로 유지하면서 각 행에 대해 특정 범위(이를 윈도우라고 합니다) 내의 값들을 사용하여 계산을 수행합니다.
덕분에 개별 행의 상세 정보와 함께 집계된 분석 값을 한눈에 볼 수 있습니다.
윈도우 함수와 집계 함수의 핵심 차이
Section titled “윈도우 함수와 집계 함수의 핵심 차이”윈도우 함수 vs 집계 함수 핵심 비교
GROUP BY 절을 통해 명시적으로 데이터 그룹화PARTITION BY 절로 윈도우 분할 (데이터 그룹화 아님)AVG(salary) FROM employees GROUP BY departmentAVG(salary) OVER (PARTITION BY department) FROM employees결국 윈도우 함수는 전체 중의 부분이 아닌, 전체 속에서 각자의 위치와 관계를 파악하는 데 특화된 도구라고 할 수 있습니다.
윈도우 함수 기본 문법 파헤치기
Section titled “윈도우 함수 기본 문법 파헤치기”윈도우 함수의 기본 구조는 다음과 같습니다.
SELECT WINDOW_FUNCTION(ARGUMENTS) OVER ( [PARTITION BY 컬럼_리스트] [ORDER BY 정렬_기준] [WINDOWING_CLAUSE] -- ROWS/RANGE BETWEEN 시작점 AND 끝점 ) AS 별칭FROM 테이블명;각 구성 요소를 자세히 살펴보겠습니다.
WINDOW_FUNCTION(ARGUMENTS): 사용하려는 윈도우 함수와 필요에 따라 전달하는 인수입니다. (예:SUM(salary),RANK())OVER(): 이 키워드가 바로 윈도우 함수임을 나타내는 핵심 부분입니다. 괄호 안에는 함수가 적용될 데이터의 범위(윈도우)를 어떻게 정의할지 명시합니다.-
PARTITION BY 컬럼_리스트(선택 사항): 전체 데이터 집합을 특정 기준(컬럼)에 따라 여러 개의 작은 그룹, 즉 파티션(Partition) 으로 나눕니다. 윈도우 함수는 각 파티션 내에서 독립적으로 계산됩니다.
예를 들어PARTITION BY department_id는 부서별로 윈도우를 설정하여 부서별 순위나 평균 등을 계산합니다. 이 절을 생략하면 전체 데이터 집합이 하나의 파티션으로 간주됩니다. -
ORDER BY 정렬_기준(선택 사항): 각 파티션 내에서 데이터를 어떤 순서로 정렬할지 지정합니다. 순위 함수(RANK(),DENSE_RANK()등)를 사용하거나, 누적 값을 계산하거나,WINDOWING_CLAUSE에서 순서 기반의 범위를 지정할 때 중요합니다. -
WINDOWING_CLAUSE(선택 사항): 함수가 계산을 수행할 파티션 내의 행 범위를 더욱 세밀하게 지정합니다. 보통ROWS BETWEEN 시작점 AND 끝점또는RANGE BETWEEN 시작점 AND 끝점형태로 사용됩니다.CURRENT ROW: 현재 처리 중인 행.n PRECEDING: 현재 행보다n개 이전 행. (UNBOUNDED PRECEDING: 파티션의 첫 번째 행)n FOLLOWING: 현재 행보다n개 이후 행. (UNBOUNDED FOLLOWING: 파티션의 마지막 행)
예를 들어,
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW는 파티션의 처음부터 현재 행까지를 범위로 지정하여 누적 합계 등을 계산하는 데 사용됩니다.
-
꼭 알아야 할 윈도우 함수 유형과 활용법
Section titled “꼭 알아야 할 윈도우 함수 유형과 활용법”윈도우 함수는 그 기능에 따라 여러 유형으로 나눌 수 있습니다.
윈도우 함수에는 정말 많은 종류가 있기 때문에 여기서는 가장 자주 사용되는 핵심 함수들을 실제 활용 예시와 함께 살펴보겠습니다.
더 자세한 내용은 윈도우 함수 목록을 참고해주세요.
순위 매기기 - RANK(), DENSE_RANK(), ROW_NUMBER()
Section titled “순위 매기기 - RANK(), DENSE_RANK(), ROW_NUMBER()”그룹 내에서 각 행의 순위를 매길 때 사용하며, ORDER BY 절이 필수적입니다.
ROW_NUMBER(): 파티션 내에서 각 행에 고유한 순번을 부여합니다. 정렬 기준값이 동일하더라도 서로 다른 순번을 가집니다. (예: 1, 2, 3, 4)RANK(): 정렬 기준값이 동일한 경우 같은 순위를 부여합니다. 하지만 다음 순위는 해당 동일 순위 개수만큼 건너뛰어 매겨집니다. (예: 1, 1, 3, 4)DENSE_RANK():RANK()와 유사하게 동일한 값에 같은 순위를 부여하지만, 다음 순위를 건너뛰지 않고 바로 다음 숫자로 매깁니다. (예: 1, 1, 2, 3)
활용 예시: 부서별로 직원들의 급여 순위를 매기고 싶을 때
SELECT employee_name, department_id, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) as rank_in_dept, DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) as dense_rank_in_dept, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) as row_num_in_deptFROM employees;가상 입력 데이터 (employees 테이블)
employees
| employee_name | department_id | salary |
|---|---|---|
| Alice | 1 | 70000 |
| Bob | 1 | 80000 |
| Charlie | 1 | 70000 |
| David | 2 | 90000 |
| Eve | 2 | 85000 |
위 쿼리 실행 시 예상 결과
순위 함수 결과
| employee_name | department_id | salary | rank_in_dept | dense_rank_in_dept | row_num_in_dept |
|---|---|---|---|---|---|
| Bob | 1 | 80000 | 1 | 1 | 1 |
| Alice | 1 | 70000 | 2 | 2 | 2 |
| Charlie | 1 | 70000 | 2 | 2 | 3 |
| David | 2 | 90000 | 1 | 1 | 1 |
| Eve | 2 | 85000 | 2 | 2 | 2 |
그룹 내 집계 - SUM(), AVG(), COUNT() 등 OVER()와 함께 사용
Section titled “그룹 내 집계 - SUM(), AVG(), COUNT() 등 OVER()와 함께 사용”일반 집계 함수처럼 합계, 평균, 개수 등을 계산하지만, GROUP BY 절 없이 사용되며 각 행에 대한 결과를 반환하며, PARTITION BY로 정의된 윈도우 내에서 집계가 이루어집니다.
활용 예시: 각 직원의 급여와 해당 직원이 속한 부서의 평균 급여, 총 급여를 함께 보고 싶을 때
SELECT employee_name, department_id, salary, AVG(salary) OVER (PARTITION BY department_id) as avg_salary_in_dept, SUM(salary) OVER (PARTITION BY department_id) as total_salary_in_deptFROM employees;이 쿼리는 각 직원 정보와 함께, 해당 직원이 속한 부서의 평균 급여와 총 급여를 각 행마다 보여줍니다. GROUP BY를 사용했을 때와 달리, 직원 개별 행이 모두 유지되는 것이 특징입니다.
가상 입력 데이터 (employees 테이블, 위와 동일)
employees
| employee_name | department_id | salary |
|---|---|---|
| Alice | 1 | 70000 |
| Bob | 1 | 80000 |
| Charlie | 1 | 70000 |
| David | 2 | 90000 |
| Eve | 2 | 85000 |
위 쿼리 실행 시 예상 결과
그룹 내 집계 함수 결과
| employee_name | department_id | salary | avg_salary_in_dept | total_salary_in_dept |
|---|---|---|---|---|
| Alice | 1 | 70000 | 73333.33 | 220000 |
| Bob | 1 | 80000 | 73333.33 | 220000 |
| Charlie | 1 | 70000 | 73333.33 | 220000 |
| David | 2 | 90000 | 87500.00 | 175000 |
| Eve | 2 | 85000 | 87500.00 | 175000 |
(참고: avg_salary_in_dept는 이해를 돕기 위해 소수점 두 자리까지 표기했습니다.)
이전/다음 행 참조 - LAG(), LEAD()
Section titled “이전/다음 행 참조 - LAG(), LEAD()”현재 행을 기준으로 특정 파티션 내에서 정렬된 순서에 따라 이전 행 또는 다음 행의 값을 가져올 때 사용합니다.
시계열 데이터 분석에서 변화량을 계산하거나 추세를 파악하는 데 매우 유용합니다.
LAG(컬럼, n, 기본값): 현재 행보다n번째 이전 행의컬럼값을 가져옵니다. 이전 행이 없으면기본값을 반환합니다.n과기본값은 생략 가능하며, 생략 시n=1이고 기본값은NULL입니다.LEAD(컬럼, n, 기본값): 현재 행보다n번째 다음 행의컬럼값을 가져옵니다. 다음 행이 없으면기본값을 반환합니다.n과기본값은 생략 가능하며, 생략 시n=1이고 기본값은NULL입니다.
활용 예시: 월별 매출 데이터에서 전월 대비 매출 증감액을 계산하고 싶을 때
SELECT sale_month, monthly_revenue, LAG(monthly_revenue, 1, 0) OVER (ORDER BY sale_month) as previous_month_revenue, monthly_revenue - LAG(monthly_revenue, 1, 0) OVER (ORDER BY sale_month) as revenue_changeFROM monthly_sales;가상 입력 데이터 (monthly_sales 테이블)
monthly_sales
| sale_month | monthly_revenue |
|---|---|
| 2023-01 | 10000 |
| 2023-02 | 12000 |
| 2023-03 | 11000 |
| 2023-04 | 15000 |
위 쿼리 실행 시 예상 결과
LAG 함수 결과 (매출 변화)
| sale_month | monthly_revenue | previous_month_revenue | revenue_change |
|---|---|---|---|
| 2023-01 | 10000 | 0 | 10000 |
| 2023-02 | 12000 | 10000 | 2000 |
| 2023-03 | 11000 | 12000 | -1000 |
| 2023-04 | 15000 | 11000 | 4000 |
그룹 내 비율 및 분포 - RATIO_TO_REPORT(), PERCENT_RANK(), CUME_DIST(), NTILE()
Section titled “그룹 내 비율 및 분포 - RATIO_TO_REPORT(), PERCENT_RANK(), CUME_DIST(), NTILE()”파티션 내에서 현재 행의 값이 차지하는 비율, 순위 기반 백분율, 누적 분포 등을 계산합니다.
RATIO_TO_REPORT(컬럼) OVER (PARTITION BY ...): 파티션 내컬럼의 전체 합계에서 현재 행의컬럼값이 차지하는 비율을 계산합니다. (0과 1 사이의 값)PERCENT_RANK() OVER (PARTITION BY ... ORDER BY ...): 파티션 내에서 현재 행의 순위 백분율을 계산합니다. 공식은(RANK() - 1) / (파티션 내 총 행 수 - 1)입니다.CUME_DIST() OVER (PARTITION BY ... ORDER BY ...): 파티션 내에서 현재 행보다 작거나 같은 값을 가진 행들의 누적 분포 비율을 계산합니다.NTILE(N) OVER (PARTITION BY ... ORDER BY ...): 파티션 내의 행들을N개의 그룹으로 최대한 균등하게 분할하고 각 행이 속한 그룹 번호를 반환합니다. (예: 상위 25%는 1, 그다음 25%는 2 등으로 그룹핑)
활용 예시: 각 제품의 매출액이 전체 매출액 또는 카테고리별 매출액에서 차지하는 비중을 알고 싶을 때
SELECT product_name, category, sales_amount, RATIO_TO_REPORT(sales_amount) OVER (PARTITION BY category) as ratio_in_category, RATIO_TO_REPORT(sales_amount) OVER () as ratio_in_total -- 전체 제품 대상FROM product_sales;가상 입력 데이터 (product_sales 테이블)
product_sales
| product_name | category | sales_amount |
|---|---|---|
| Laptop | Tech | 1200 |
| Mouse | Tech | 50 |
| Keyboard | Tech | 150 |
| Book | Books | 30 |
| Magazine | Books | 20 |
위 쿼리 실행 시 예상 결과
RATIO_TO_REPORT 함수 결과 (매출 비중)
| product_name | category | sales_amount | ratio_in_category | ratio_in_total |
|---|---|---|---|---|
| Laptop | Tech | 1200 | 0.857 | 0.828 |
| Mouse | Tech | 50 | 0.036 | 0.034 |
| Keyboard | Tech | 150 | 0.107 | 0.103 |
| Book | Books | 30 | 0.600 | 0.021 |
| Magazine | Books | 20 | 0.400 | 0.014 |
(참고: 비율은 이해를 돕기 위해 소수점 세 자리까지 표기했습니다.)
윈도우 함수, 성능까지 생각하며 사용하기
Section titled “윈도우 함수, 성능까지 생각하며 사용하기”윈도우 함수는 매우 편리한 기능이지만, 내부적으로 정렬(Sort)이나 복잡한 연산을 수행하는 경우가 있어 대용량 데이터를 처리할 때 성능에 영향을 미칠 수 있는데요, 따라서 윈도우 함수를 효율적으로 사용하기 위해서는 몇 가지 사항을 고려하는 것이 좋습니다.
먼저, 윈도우 함수가 처리해야 할 데이터의 양을 최소화하는 것이 중요합니다.
윈도우 함수를 적용하기 전에 WHERE 절을 사용하여 분석 대상을 최대한 줄이면, 처리할 데이터가 적어져 윈도우 계산 속도를 높일 수 있습니다.
또한, 서브쿼리나 CTE(Common Table Expression, 공통 테이블 표현식)를 활용하여 윈도우 함수가 연산할 레코드 수를 미리 줄이는 것도 매우 효과적인 방법입니다.
다음으로, 적절한 인덱스를 활용하는 것이 성능 향상에 도움이 될 수 있습니다.
PARTITION BY 절이나 ORDER BY 절에 사용되는 컬럼에 인덱스가 존재한다면, 데이터베이스 옵티마이저가 이 인덱스를 활용하여 파티셔닝이나 정렬 작업을 더 신속하게 수행할 수 있기 때문입니다.
불필요한 정렬을 최소화하는 것도 중요합니다.
모든 윈도우 함수가 ORDER BY 절을 필요로 하는 것은 아닙니다.
예를 들어 SUM() OVER (PARTITION BY ...)와 같이 파티션 내 전체 집계를 수행하는 경우 ORDER BY가 없어도 문제 없이 동작합니다.
순위 함수(RANK(), ROW_NUMBER() 등)나 LAG/LEAD 함수처럼 순서가 중요한 특정 함수의 기능을 사용하거나, 윈도우 프레임(WINDOWING_CLAUSE)의 논리적 범위를 정밀하게 지정해야 할 때만 ORDER BY를 사용하는 등 불필요한 정렬을 피하는 것이 좋습니다.
마지막으로, 윈도우 프레임(Window Frame)을 현명하게 사용해야 합니다.
WINDOWING_CLAUSE에서 UNBOUNDED PRECEDING이나 UNBOUNDED FOLLOWING과 같이 매우 넓은 범위를 지정하면 계산량이 크게 증가할 수 있습니다.
특히 RANGE를 기준으로 프레임을 정의할 때 정렬 기준 값에 중복이 많을 경우, 예상치 못한 성능 저하가 발생할 수 있으므로 주의해야 합니다.
가능하다면 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW처럼 필요한 만큼으로 범위를 명시적으로 제한하고, 데이터의 특성을 고려하여 ROWS 또는 RANGE 방식 중 적절한 것을 선택하는 것이 좋습니다.
이처럼 테이블 스캔 횟수가 적다는 점 때문에 윈도우 함수가 항상 여러 번의 조인(Join)이나 셀프 조인(Self Join), 혹은 스칼라 서브쿼리(Scalar Subquery)를 사용하는 다른 방법보다 성능이 우수하다고 단정하기는 어렵습니다.
실제 실행 계획을 확인하고, 분석하려는 데이터의 특성, 전체 데이터의 양, 그리고 사용 중인 데이터베이스 관리 시스템(DBMS)의 최적화 능력 등을 종합적으로 고려하였을 때 비로소 윈도우 함수의 진가를 확인할 수 있습니다.
SQL 윈도우 함수는 복잡해 보이는 데이터 분석 요구사항을 간단하게 해결시켜주는 유용한 도구입니다.
단순히 순위를 매기고 집계를 하는 것을 넘어, 데이터 행 간의 관계를 파악하고 시계열 분석, 비율 계산 등 다양한 고급 분석을 SQL만으로 수행할 수 있게 해줍니다.
오늘 살펴본 것처럼 윈도우 함수의 기본 개념과 문법을 정확히 이해하고, 다양한 함수 유형을 적재적소에 활용하며 성능 측면까지 고려한다면, 데이터 분석의 효율성과 깊이를 한층 끌어올릴 수 있을 것입니다.