SELECT
name
, dept
, sal
, FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY sal DESC) AS top_name
FROM emp
ORDER BY dept, sal DESC;NAME | DEPT | SAL | TOP_NAME
-------+------+------+---------
이부장 | 개발 | 800 | 이부장
박과장 | 개발 | 700 | 이부장
최대리 | 개발 | 600 | 이부장
김대표 | 경영 | 1000 | 김대표
정사원 | 영업 | 500 | 정사원
강사원 | 영업 | 480 | 정사원
(6행)급여 내림차순에서 첫 행이 최고 급여자이므로 그룹 전체가 그 이름을 받습니다. 기본 프레임의 시작이 그룹 첫 행이라 프레임을 따로 쓰지 않아도 됩니다.
같은 방식으로 FIRST_VALUE(sal) OVER (...) - sal 을 계산하면 최고 급여와의 차이가 됩니다. 개발팀은 0, 100, 200 이고 영업팀은 0, 20 입니다.
SELECT
name
, dept
, sal
, LAST_VALUE(name) OVER (PARTITION BY dept ORDER BY sal DESC) AS last_name
FROM emp
ORDER BY dept, sal DESC;NAME | DEPT | SAL | LAST_NAME
-------+------+------+----------
이부장 | 개발 | 800 | 이부장
박과장 | 개발 | 700 | 박과장
최대리 | 개발 | 600 | 최대리
김대표 | 경영 | 1000 | 김대표
정사원 | 영업 | 500 | 정사원
강사원 | 영업 | 480 | 강사원
(6행)부서 최저 급여자를 기대했지만 모든 행이 자기 이름을 받았습니다. 프레임이 현재 행까지라서 마지막 값이 현재 행 자신입니다. 프레임 끝을 그룹 끝까지 늘려 고칩니다.
SELECT
name
, dept
, sal
, LAST_VALUE(name) OVER (PARTITION BY dept ORDER BY sal DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS low_name
FROM emp
ORDER BY dept, sal DESC;NAME | DEPT | SAL | LOW_NAME
-------+------+------+---------
이부장 | 개발 | 800 | 최대리
박과장 | 개발 | 700 | 최대리
최대리 | 개발 | 600 | 최대리
김대표 | 경영 | 1000 | 김대표
정사원 | 영업 | 500 | 강사원
강사원 | 영업 | 480 | 강사원
(6행)주의
LAST_VALUE는 오류 없이 자기 자신을 돌려주므로 결과가 그럴듯해 보여 놓치기 쉽습니다.LAST_VALUE를 쓸 때는 프레임을UNBOUNDED FOLLOWING까지 명시하거나, 정렬 방향을 뒤집어(ORDER BY sal ASC)FIRST_VALUE를 씁니다.
NTH_VALUE(x, n) 은 프레임의 n번째 행 값을 가져옵니다. H2 2.3.232 에서 실행이 확인되었습니다. 프레임의 영향을 받으므로 LAST_VALUE 처럼 프레임을 끝까지 늘립니다.
SELECT
name
, dept
, sal
, NTH_VALUE(name, 2) OVER (PARTITION BY dept ORDER BY sal DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS second_name
FROM emp
ORDER BY dept, sal DESC;NAME | DEPT | SAL | SECOND_NAME
-------+------+------+------------
이부장 | 개발 | 800 | 박과장
박과장 | 개발 | 700 | 박과장
최대리 | 개발 | 600 | 박과장
김대표 | 경영 | 1000 | NULL
정사원 | 영업 | 500 | 강사원
강사원 | 영업 | 480 | 강사원
(6행)경영팀은 1명뿐이라 2번째 행이 없어 NULL 입니다. NTH_VALUE 는 Oracle 11gR2·MySQL 8.0 에서 되고 MSSQL 에는 없습니다.
날짜 간격 표기는 DB 마다 다릅니다. 예제 4 의 간격 계산을 DB 별로 쓰면 다음과 같습니다.
-- Oracle · Tibero: 날짜끼리 빼면 일수
SELECT ord_date - LAG(ord_date) OVER (PARTITION BY emp_id ORDER BY ord_date) AS gap_days FROM orders;
-- MySQL: DATEDIFF(뒤, 앞) 이 뒤 - 앞 일수
SELECT DATEDIFF(ord_date, LAG(ord_date) OVER (PARTITION BY emp_id ORDER BY ord_date)) AS gap_days FROM orders;
-- MSSQL: DATEDIFF(단위, 앞, 뒤) 는 경계 횟수
SELECT DATEDIFF(DAY, LAG(ord_date) OVER (PARTITION BY emp_id ORDER BY ord_date), ord_date) AS gap_days FROM orders;위 세 문장은 표기를 보이려는 요약이며 H2 로 실행하지 않았습니다. 인자 순서가 반대이므로 옮길 때 부호가 뒤집히지 않게 확인합니다.
전월 대비 LAG 문법은 Oracle·Tibero·MySQL 8.0·MSSQL 2012 이상에서 같습니다. 03 파일은 같은 쿼리를 SET MODE 별로 돌렸고 결과가 세 모드에서 같았습니다.
-- Oracle · Tibero, MySQL 8.0, MSSQL 2012 이상 모두 같은 문장
-- monthly CTE 동일
SELECT
mon
, sales
, sales - LAG(sales) OVER (ORDER BY mon) AS diff
FROM monthly
ORDER BY mon;MON | SALES | DIFF
----+-------+-----
1 | 1200 | NULL
2 | 700 | -500
3 | 800 | 100
4 | 600 | -200
(4행)H2 는 방언을 흉내 낼 뿐이라 이 결과가 실제 DB 동작의 근거는 아닙니다. 문법이 같다는 근거는 2.4절의 버전 조건입니다.
MSSQL 2008 이하에서는 월 번호로 셀프 조인(LEFT JOIN monthly p ON p.mon = c.mon - 1)해 대체합니다. 빠진 달이 있으면 NULL 이 나오니 LAG 와 의미가 다릅니다.
서비스 상태를 하루 한 행씩 기록한 svc_log(day_no, status) 가 있습니다. 앞 행과 상태가 다른 행만 골라 "언제 상태가 바뀌었나"를 찾습니다. 윈도 함수는 WHERE 에 쓸 수 없어 인라인 뷰로 감쌉니다.
SELECT
day_no
, status
, prev_status
FROM (
SELECT
day_no
, status
, LAG(status) OVER (ORDER BY day_no) AS prev_status
FROM svc_log
) t
WHERE prev_status IS NULL
OR prev_status <> status
ORDER BY day_no;DAY_NO | STATUS | PREV_STATUS
-------+--------+------------
1 | 정상 | NULL
3 | 장애 | 정상
6 | 정상 | 장애
7 | 장애 | 정상
8 | 정상 | 장애
(5행)첫 행은 prev_status 가 NULL 이라서 <> 비교가 참이 되지 않습니다. IS NULL 조건을 함께 써야 첫 행이 남습니다. 이 조건을 빼먹으면 시작 상태가 결과에서 사라집니다.
같은 상태가 연속된 구간의 시작·끝·길이를 구하는 패턴입니다. 전체 순번과 상태별 순번의 차이가 같은 행끼리 한 구간이 되는 성질을 씁니다.
SELECT
status
, MIN(day_no) AS from_day
, MAX(day_no) AS to_day
, COUNT(*) AS days
FROM (
SELECT
day_no
, status
, day_no - ROW_NUMBER() OVER (PARTITION BY status ORDER BY day_no) AS grp
FROM svc_log
) t
GROUP BY status, grp
ORDER BY from_day;STATUS | FROM_DAY | TO_DAY | DAYS
-------+----------+--------+-----
정상 | 1 | 2 | 2
장애 | 3 | 5 | 3
정상 | 6 | 6 | 1
장애 | 7 | 7 | 1
정상 | 8 | 8 | 1
(5행)정상 상태의 행은 day_no 가 1, 2, 6, 8 이고 상태별 순번은 1, 2, 3, 4 입니다. 차이(grp)가 0, 0, 3, 4 로 갈라지는 곳이 구간 경계입니다.
이 예제는 day_no 가 정수라 바로 뺐습니다. 날짜 컬럼이면 날짜에서 순번만큼의 일수를 빼서 같은 원리로 씁니다.