공공부하자개발 · 영어 학습 노트
SQL
SQL 고급분석 함수·계층·피벗·집계 확장0/10 완료
  • 01순위 분석 함수
  • 02집계 분석 함수와 윈도 프레임
  • 03행 비교 분석 함수
  • 04계층 쿼리: CONNECT BY 와 재귀 CTE
  • 05행과 열 바꾸기
  • 06소계와 총계
  • 07WITH 절(CTE)로 쿼리 구조화
  • 08MERGE 와 UPSERT
  • 09정규식 함수
  • 10다른 테이블 기준으로 수정·삭제
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › SQL 고급 › 03 / 10

행 비교 분석 함수

LAG, LEAD, FIRST_VALUE, LAST_VALUE
섹션 6진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6정리‹ 이전다음 ›

4. 응용 변형 예제

변형 1: FIRST_VALUE 로 부서 최고 급여자

sql
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;
text
NAME   | DEPT | SAL  | TOP_NAME
-------+------+------+---------
이부장 | 개발 | 800  | 이부장
박과장 | 개발 | 700  | 이부장
최대리 | 개발 | 600  | 이부장
김대표 | 경영 | 1000 | 김대표
정사원 | 영업 | 500  | 정사원
강사원 | 영업 | 480  | 정사원
(6행)

급여 내림차순에서 첫 행이 최고 급여자이므로 그룹 전체가 그 이름을 받습니다. 기본 프레임의 시작이 그룹 첫 행이라 프레임을 따로 쓰지 않아도 됩니다.

같은 방식으로 FIRST_VALUE(sal) OVER (...) - sal 을 계산하면 최고 급여와의 차이가 됩니다. 개발팀은 0, 100, 200 이고 영업팀은 0, 20 입니다.

변형 2: LAST_VALUE 기본 프레임 함정

sql
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;
text
NAME   | DEPT | SAL  | LAST_NAME
-------+------+------+----------
이부장 | 개발 | 800  | 이부장
박과장 | 개발 | 700  | 박과장
최대리 | 개발 | 600  | 최대리
김대표 | 경영 | 1000 | 김대표
정사원 | 영업 | 500  | 정사원
강사원 | 영업 | 480  | 강사원
(6행)

부서 최저 급여자를 기대했지만 모든 행이 자기 이름을 받았습니다. 프레임이 현재 행까지라서 마지막 값이 현재 행 자신입니다. 프레임 끝을 그룹 끝까지 늘려 고칩니다.

sql
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;
text
NAME   | DEPT | SAL  | LOW_NAME
-------+------+------+---------
이부장 | 개발 | 800  | 최대리
박과장 | 개발 | 700  | 최대리
최대리 | 개발 | 600  | 최대리
김대표 | 경영 | 1000 | 김대표
정사원 | 영업 | 500  | 강사원
강사원 | 영업 | 480  | 강사원
(6행)
주의

LAST_VALUE 는 오류 없이 자기 자신을 돌려주므로 결과가 그럴듯해 보여 놓치기 쉽습니다. LAST_VALUE 를 쓸 때는 프레임을 UNBOUNDED FOLLOWING 까지 명시하거나, 정렬 방향을 뒤집어(ORDER BY sal ASC) FIRST_VALUE 를 씁니다.

변형 3: NTH_VALUE 와 DB 별 날짜 간격

NTH_VALUE(x, n) 은 프레임의 n번째 행 값을 가져옵니다. H2 2.3.232 에서 실행이 확인되었습니다. 프레임의 영향을 받으므로 LAST_VALUE 처럼 프레임을 끝까지 늘립니다.

sql
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;
text
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 별로 쓰면 다음과 같습니다.

sql
-- 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 로 실행하지 않았습니다. 인자 순서가 반대이므로 옮길 때 부호가 뒤집히지 않게 확인합니다.

변형 4: DB 별 전월 대비 문법

전월 대비 LAG 문법은 Oracle·Tibero·MySQL 8.0·MSSQL 2012 이상에서 같습니다. 03 파일은 같은 쿼리를 SET MODE 별로 돌렸고 결과가 세 모드에서 같았습니다.

sql
-- 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;
text
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 와 의미가 다릅니다.

변형 5: 상태가 바뀐 행만 뽑기

서비스 상태를 하루 한 행씩 기록한 svc_log(day_no, status) 가 있습니다. 앞 행과 상태가 다른 행만 골라 "언제 상태가 바뀌었나"를 찾습니다. 윈도 함수는 WHERE 에 쓸 수 없어 인라인 뷰로 감쌉니다.

sql
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;
text
DAY_NO | STATUS | PREV_STATUS
-------+--------+------------
1      | 정상   | NULL
3      | 장애   | 정상
6      | 정상   | 장애
7      | 장애   | 정상
8      | 정상   | 장애
(5행)

첫 행은 prev_status 가 NULL 이라서 <> 비교가 참이 되지 않습니다. IS NULL 조건을 함께 써야 첫 행이 남습니다. 이 조건을 빼먹으면 시작 상태가 결과에서 사라집니다.

변형 6: 연속 구간 묶기 (gaps and islands)

같은 상태가 연속된 구간의 시작·끝·길이를 구하는 패턴입니다. 전체 순번과 상태별 순번의 차이가 같은 행끼리 한 구간이 되는 성질을 씁니다.

sql
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;
text
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 가 정수라 바로 뺐습니다. 날짜 컬럼이면 날짜에서 순번만큼의 일수를 빼서 같은 원리로 씁니다.

응용 변형 예제
  • 변형 1: FIRST_VALUE 로 부서 최고 급여자
  • 변형 2: LAST_VALUE 기본 프레임 함정
  • 변형 3: NTH_VALUE 와 DB 별 날짜 간격
  • 변형 4: DB 별 전월 대비 문법
  • 변형 5: 상태가 바뀐 행만 뽑기
  • 변형 6: 연속 구간 묶기 (gaps and islands)
이전 섹션3 코드 예제4 / 6다음 섹션5 자주 하는 실수 (Tip)