by 마농 분석함수 RANGE BETWEEN ROWS BETWEEN NUMTOYMINTERVAL NUMTODSINTERVAL UNBOUNDED PRECEDING [2015.06.09]
이번 퀴즈로 배워보는 SQL 시간에는 분석 함수(Analytic Function)를 이용해 합계를 구하는 문제를 풀어본다. 지면 특성상 문제와 정답 그리고 해설이 같이 있다. 진정으로 자신의 SQL 실력을 키우고 싶다면 스스로 문제를 해결한 다음 정답과 해설을 참조하길 바란다. 공부를 잘하는 학생의 문제집은 항상 문제지면의 밑바닥은 까맣지만 정답과 해설지면은 하얗다는 사실을 기억하자.
다음과 같이 월별 금액을 가진 집합에서 해당 월을 기준으로 이전 3개월간 금액 합계와 이후 3개월간 금액 합계를 함께 출력하는 SQL을 작성하세요.
WITH t AS ( SELECT '201201' yyyymm, 100 amt FROM dual UNION ALL SELECT '201202', 200 FROM dual UNION ALL SELECT '201203', 300 FROM dual UNION ALL SELECT '201204', 400 FROM dual UNION ALL SELECT '201205', 500 FROM dual UNION ALL SELECT '201206', 600 FROM dual UNION ALL SELECT '201207', 700 FROM dual UNION ALL SELECT '201208', 800 FROM dual UNION ALL SELECT '201209', 900 FROM dual UNION ALL SELECT '201210', 100 FROM dual UNION ALL SELECT '201211', 200 FROM dual UNION ALL SELECT '201212', 300 FROM dual ) SELECT * FROM t ; YYYYMM AMT ------ ---------- 201201 100 201202 200 201203 300 201204 400 201205 500 201206 600 201207 700 201208 800 201209 900 201210 100 201211 200 201212 300
이 문제는 연도와 월별로 특정 금액을 가진 집합에서 해당 연월을 기준으로 이전 3개월간의 금액 합계와 이후 3개월간의 금액 합계를 구하는 문제입니다.
[표 2]의 결과를 보면 첫 번째 달인 1월에는 직전 3개월에 해당하는 자료가 없으므로 Null 값이 나오고, 이후 3개월에 속하는 2, 3, 4월의 합계는 200, 300, 400을 더한 900이 나왔습니다.
이처럼 2월에는 1월 합계와 3, 4, 5월 합계, 3월에는 1, 2월 합계와 4, 5, 6월 합계를 보여주면 됩니다.
문제를 스스로 해결해 보셨나요? 이제 정답을 알아보겠습니다.
WITH t AS ( SELECT '201201' yyyymm, 100 amt FROM dual UNION ALL SELECT '201202', 200 FROM dual UNION ALL SELECT '201203', 300 FROM dual UNION ALL SELECT '201204', 400 FROM dual UNION ALL SELECT '201205', 500 FROM dual UNION ALL SELECT '201206', 600 FROM dual UNION ALL SELECT '201207', 700 FROM dual UNION ALL SELECT '201208', 800 FROM dual UNION ALL SELECT '201209', 900 FROM dual UNION ALL SELECT '201210', 100 FROM dual UNION ALL SELECT '201211', 200 FROM dual UNION ALL SELECT '201212', 300 FROM dual ) SELECT yyyymm , amt , SUM(amt) OVER(ORDER BY TO_DATE(yyyymm,'yyyymm') RANGE BETWEEN INTERVAL '3' MONTH PRECEDING AND INTERVAL '1' MONTH PRECEDING) amt_pre3 , SUM(amt) OVER(ORDER BY TO_DATE(yyyymm,'yyyymm') RANGE BETWEEN INTERVAL '1' MONTH FOLLOWING AND INTERVAL '3' MONTH FOLLOWING) amt_fol3 FROM t ; YYYYMM AMT AMT_PRE3 AMT_FOL3 ------ ---------- ---------- ---------- 201201 100 900 201202 200 100 1200 201203 300 300 1500 201204 400 600 1800 201205 500 900 2100 201206 600 1200 2400 201207 700 1500 1800 201208 800 1800 1200 201209 900 2100 600 201210 100 2400 500 201211 200 1800 300 201212 300 1200
어떤가요? 여러분이 만들어본 리스트와 같은가요?
틀렸다고 좌절할 필요는 없답니다. 첫 술에 배부를 순 없는 것이니까요. 해설을 꼼꼼히 보고 자신이 잘못한 점을 비교해 보는 것이 더 중요합니다.
이번 문제는 분석 함수의 기본적인 사용법에 대해 익히는 문제입니다. 분석 함수에서 잘 사용하기 않는 기능 하나를 배우는 몸풀기 문제라고 보시면 됩니다. 우선 분석 함수를 이용하지 않고 문제를 풀어본 다음, 분석 함수를 적용해 풀어본 답과 비교해보겠습니다.
WITH t AS ( SELECT '201201' yyyymm, 100 amt FROM dual UNION ALL SELECT '201202', 200 FROM dual UNION ALL SELECT '201203', 300 FROM dual UNION ALL SELECT '201204', 400 FROM dual UNION ALL SELECT '201205', 500 FROM dual UNION ALL SELECT '201206', 600 FROM dual UNION ALL SELECT '201207', 700 FROM dual UNION ALL SELECT '201208', 800 FROM dual UNION ALL SELECT '201209', 900 FROM dual UNION ALL SELECT '201210', 100 FROM dual UNION ALL SELECT '201211', 200 FROM dual UNION ALL SELECT '201212', 300 FROM dual ) SELECT a.yyyymm , a.amt , SUM(CASE WHEN b.yyyymm < a.yyyymm THEN b.amt END) amt_pre3 , SUM(CASE WHEN b.yyyymm > a.yyyymm THEN b.amt END) amt_fol3 FROM t a , t b WHERE b.yyyymm >= TO_CHAR(ADD_MONTHS(TO_DATE(a.yyyymm, 'yyyymm'), -3), 'yyyymm') AND b.yyyymm <= TO_CHAR(ADD_MONTHS(TO_DATE(a.yyyymm, 'yyyymm'), 3), 'yyyymm') GROUP BY a.yyyymm, a.amt ORDER BY a.yyyymm ; YYYYMM AMT AMT_PRE3 AMT_FOL3 ------ ---------- ---------- ---------- 201201 100 900 201202 200 100 1200 201203 300 300 1500 201204 400 600 1800 201205 500 900 2100 201206 600 1200 2400 201207 700 1500 1800 201208 800 1800 1200 201209 900 2100 600 201210 100 2400 500 201211 200 1800 300 201212 300 1200
[리스트 3]은 분석 함수 대신 셀프 조인을 이용해 문제를 해결한 것입니다. 내용을 살펴보면 같은 테이블을 두 번 읽어 서로 범위를 비교해 조인한 다음, 기준이 되는 테이블로 그룹 바이해 비교 대상 금액을 합산하는 방식입니다. 설명에서도 알 수 있듯이 같은 테이블을 두 번이나 읽어 조인해야 하므로 효율성이 떨어집니다.
그러나 분석 함수를 사용하면 테이블을 한 번만 읽고도 문제를 해결할 수 있으니 매우 효율적이겠죠? 정답 쿼리를 설명하기 전 우리가 흔히 사용하는 누적 합계를 구하는 SQL부터 살펴보겠습니다.
SELECT yyyymm , amt , SUM(amt) OVER(ORDER BY yyyymm) amt_tot FROM t ; YYYYMM AMT AMT_TOT ------ ---------- ---------- 201201 100 100 201202 200 300 201203 300 600 201204 400 1000 201205 500 1500 201206 600 2100 201207 700 2800 201208 800 3600 201209 900 4500 201210 100 4600 201211 200 4800 201212 300 5100
[표 3]의 결과를 보면 위에서부터 차례대로 금액이 누적되는 것을 볼 수 있습니다. [리스트 4]에 있는 구문들을 좀더 자세히 알아보죠.
, SUM(amt) OVER(ORDER BY yyyymm) amt_tot
이 구문은 yyyymm으로 정렬하면서 amt 값을 합산하라는 의미입니다. 간단한 구문만으로도 아주 쉽게 누적 합계를 구할 수 있습니다. 하지만 우리가 지금 구하려고 하는 것은 처음부터 해당 월까지의 누적 합계가 아니라 바로 이전 3개월 동안의 값을 더한 것입니다.
그렇다면 분석 함수에서 구간을 정해줄 수 있는 구문이 있다면 좋겠네요? 사실 앞서 살펴본 누적 합계 구문도 다음과 같이 구간을 정해주는 구문이 생략된 형태입니다.
, SUM(amt) OVER( ORDER BY yyyymm ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) amt_tot
여기서 잠깐, 집계 함수와 분석 함수의 차이점을 살펴보도록 하죠. 집계 함수와 분석 함수의 차이는 결과 행의 수가 다르다는 데 있습니다. 집계 함수는 결과를 집약하는 것인 만큼 레코드 수가 줄어들지만, 분석 함수는 레코드 수가 줄어들지 않은 상태 그대로를 보여주면서 집계 결과도 함께 보여줍니다.
구문상에서 보면 SUM이라고 하는 집계 함수에 OVER라는 구문이 붙어서 분석 함수가 된다는 걸 알 수 있습니다. 원래 OVER 안에는 그룹을 지정해주는 PARTITION BY 라는 구문이 있는데 여기서는 생략됐죠. 그 다음에는 순서를 지정해주는 ORDER BY 구문이 있고 집계 대상의 구간을 지정해주는 ROWS 또는 RANGE와 같은 구문이 있다는 걸 알 수 있습니다.
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 특히 이 부분이 생략된 경우라면 기본(Default) 값을 나타내고 있는 것 입니다. 위 구문을 해석해 보면, 맨앞 행(UNBOUNDED PRECEDING)에서부터 현재 행(CURRENT ROW)까지로 구간을 한정하고 있습니다.
따라서 이 구문을 생략하지 않고 구간 정보를 변경한 다음 실행한다면 원하는 결과를 얻을 수 있을 것 같습니다. 3행 이전부터 1행 이전까지 그리고 1행 이후부터 3행 이후까지로 구간을 한정해보겠습니다.
SELECT yyyymm , amt , SUM(amt) OVER(ORDER BY yyyymm ROWS BETWEEN '3' PRECEDING AND '1' PRECEDING) amt_pre3 , SUM(amt) OVER(ORDER BY yyyymm ROWS BETWEEN '1' FOLLOWING AND '3' FOLLOWING) amt_fol3 FROM t ; YYYYMM AMT AMT_PRE3 AMT_FOL3 ------ ---------- ---------- ---------- 201201 100 900 201202 200 100 1200 201203 300 300 1500 201204 400 600 1800 201205 500 900 2100 201206 600 1200 2400 201207 700 1500 1800 201208 800 1800 1200 201209 900 2100 600 201210 100 2400 500 201211 200 1800 300 201212 300 1200
[리스트 5]의 쿼리 실행결과가 [표 2]의 결과 테이블과 같음을 확인할 수 있습니다. 그렇다면 ROWS BETWEEN 구문이 정답일까요? 예제에서는 1년 12달 자료가 모두 있습니다. 하지만 만약 중간에 누락된 달이 있다면 어떻게 될까요? 4월 자료를 삭제한 다음 결과를 직접 확인해보겠습니다.
WITH t AS ( SELECT '201201' yyyymm, 100 amt FROM dual UNION ALL SELECT '201202', 200 FROM dual UNION ALL SELECT '201203', 300 FROM dual -- UNION ALL SELECT '201204', 400 FROM dual -- 삭제 UNION ALL SELECT '201205', 500 FROM dual UNION ALL SELECT '201206', 600 FROM dual UNION ALL SELECT '201207', 700 FROM dual UNION ALL SELECT '201208', 800 FROM dual UNION ALL SELECT '201209', 900 FROM dual UNION ALL SELECT '201210', 100 FROM dual UNION ALL SELECT '201211', 200 FROM dual UNION ALL SELECT '201212', 300 FROM dual ) SELECT yyyymm , amt , SUM(amt) OVER(ORDER BY yyyymm ROWS BETWEEN '3' PRECEDING AND '1' PRECEDING) amt_pre3 , SUM(amt) OVER(ORDER BY yyyymm ROWS BETWEEN '1' FOLLOWING AND '3' FOLLOWING) amt_fol3 FROM t ; YYYYMM AMT AMT_PRE3 AMT_FOL3 ------ ---------- ---------- ---------- 201201 100 1000 201202 200 100 1400 201203 300 300 1800 201205 500 600 2100 201206 600 1000 2400 201207 700 1400 1800 201208 800 1800 1200 201209 900 2100 600 201210 100 2400 500 201211 200 1800 300 201212 300 1200
[표 4]의 결과를 보면 5월을 기준으로 이전 3개월 자료는 2, 3, 4월의 합계가 나와야 하는데 1, 2, 3월의 합계가 나왔습니다. 즉 ROWS BETWEEN 구문을 사용하면 오류의 소지가 있다는 것이지요. 즉, 3개 행이라는 행 단위 구간이 아닌 3개월이라는 시간 단위 구간으로 확인돼야 합니다.
이 때 사용하는 구문이 RANGE BETWEEN입니다. 하지만 RANGE BETWEEN 구문을 사용하기 위해서는 몇 가지 조건이 있습니다. 우선 ORDER BY 구문에 들어가는 항목이 반드시 시간을 나타낼 수 있는 Date형이어야 합니다.
따라서 문자형인 yyyymm 항목을 날짜로 형을 변환해야 합니다. 또한 BETWEEN 절에 들어갈 구간 값은 INTERVAL 형태로 주어져야 합니다. 모든 과정을 거치고 나니 정답 구문이 완성됐습니다.
, SUM(amt) OVER(ORDER BY TO_DATE(yyyymm,'yyyymm') RANGE BETWEEN INTERVAL '3' MONTH PRECEDING AND INTERVAL '1' MONTH PRECEDING) amt_pre3 , SUM(amt) OVER(ORDER BY TO_DATE(yyyymm,'yyyymm') RANGE BETWEEN INTERVAL '1' MONTH FOLLOWING AND INTERVAL '3' MONTH FOLLOWING) amt_fol3
여기서 한 가지 팁을 소개하면, INTERVAL 구문은 다른 형태로도 사용할 수 있습니다.
INTERVAL '3' MONTH NUMTOYMINTERVAL(3, 'MONTH')
INTERVAL에 사용할 수 있는 것은 MONTH만 있는 게 아니겠지요? [표 5]를 참고해 다양한 형태의 구간 정보에 대한 부분 합계를 구해보세요.
이번 시간에는 분석 함수의 숨은 기능에 대해 살펴봤습니다. 어떤가요? 분석 함수에 대해 확실하게 이해가 되셨나요? 대부분 분석 함수에서 PARTITION BY와 ORDER BY 정도만 사용했을 텐데, 이번 시간을 통해 분석 함수에서 범위를 제한할 수 있는 ROWS/RANGE 구문도 있다는 걸 알아두길 바랍니다.
- 강좌 URL : http://www.gurubee.net/lecture/2829
- 구루비 강좌는 개인의 학습용으로만 사용 할 수 있으며, 다른 웹 페이지에 게재할 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^^
- 구루비 강좌는 서비스 제공을 위한 목적이나, 학원 홍보, 수익을 얻기 위한 용도로 사용 할 수 없습니다.
[리스트 3] 셀프 조인을 이용한 문제 풀이 <-- 이걸 이용하셔야 할 듯.
WITH t AS ( SELECT '201201' yyyymm, 100 amt UNION ALL SELECT '201202', 200 UNION ALL SELECT '201203', 300 --UNION ALL SELECT '201204', 400 UNION ALL SELECT '201205', 500 UNION ALL SELECT '201206', 600 UNION ALL SELECT '201207', 700 UNION ALL SELECT '201208', 800 UNION ALL SELECT '201209', 900 UNION ALL SELECT '201210', 100 UNION ALL SELECT '201211', 200 UNION ALL SELECT '201212', 300 ) SELECT a.yyyymm , a.amt , SUM(CASE WHEN b.yyyymm < a.yyyymm THEN b.amt END) amt_pre3 , SUM(CASE WHEN b.yyyymm > a.yyyymm THEN b.amt END) amt_fol3 FROM t a , t b WHERE b.yyyymm >= CONVERT(VARCHAR(6), DATEADD(Month, -3, CONCAT(a.yyyymm, '01')), 112) AND b.yyyymm <= CONVERT(VARCHAR(6), DATEADD(Month, 3, CONCAT(a.yyyymm, '01')), 112) GROUP BY a.yyyymm, a.amt ORDER BY a.yyyymm ;