MSSQL 2.7 · 2부. 조회 심화

윈도 함수

OVER 로 행을 묶지 않고 셈합니다. 순위 셋의 차이와 그룹마다 상위 N 을 뽑는 정석을 봅니다.

예상 학습 시간 22분 난이도 중급
개념 설명

묶지 않고 셈합니다

2.2 의 GROUP BY 는 여러 행을 한 행으로 줄입니다. 그래서 묶지 않은 열은 낼 수 없었습니다(오류 8120).

윈도 함수는 행을 그대로 두고 집계값을 옆에 덧붙입니다. 각 행이 "내가 속한 무리에서 몇 등인지", "그 무리의 평균이 얼마인지" 를 함께 가질 수 있습니다.

문법은 집계 함수 뒤에 OVER 를 붙이는 것입니다. 괄호가 비어 있으면 결과 전체가 한 무리입니다.

SQL
SELECT TOP 5 num, board_num, hit_count,
       AVG(hit_count) OVER ()                        AS 전체평균,
       AVG(hit_count) OVER (PARTITION BY board_num) AS 게시판평균
FROM Board.POSTS
ORDER BY num;
결과
numboard_numhit_count전체평균게시판평균
127194188
2214194188
3221194188
4228194188
5235194188
(5개 행이 영향을 받음)

행이 줄지 않았습니다. 각 글이 자기 값을 그대로 가지면서 전체 평균과 제 게시판의 평균을 함께 얻었습니다. PARTITION BYGROUP BY 자리에 해당하지만, 나누기만 하고 줄이지는 않습니다.

최소 예제

순위 매기기 — 셋의 차이

순위를 매기는 함수가 셋인데 동점을 어떻게 다루는지가 다릅니다. 조회수가 겹치는 구간으로 한 번에 봅니다.

SQL
SELECT num, hit_count,
       ROW_NUMBER() OVER (ORDER BY hit_count DESC) AS row_num,
       RANK()       OVER (ORDER BY hit_count DESC) AS rank_,
       DENSE_RANK() OVER (ORDER BY hit_count DESC) AS dense_
FROM Board.POSTS
WHERE hit_count BETWEEN 194 AND 198
ORDER BY hit_count DESC, num;
결과
numhit_countrow_numrank_dense_
314198111
370198211
171197332
28196443
390196543
242194664
410194764
(7개 행이 영향을 받음)
함수동점을다음 번호
ROW_NUMBER구분합니다늘 1씩 — 1·2·3·4·5·6·7
RANK같은 등수건너뜁니다 — 1·1·3·4·4·6·6
DENSE_RANK같은 등수건너뛰지 않습니다 — 1·1·2·3·3·4·4

공동 1위가 둘이면 다음은 3위인가 2위인가. 그 답이 다른 것입니다. "공동 1위, 공동 1위, 3위" 가 흔한 순위표라면 RANK, 등급을 매기는 것처럼 번호가 끊기면 안 되면 DENSE_RANK 입니다.

ROW_NUMBER 는 동점이어도 번호를 겹치지 않게 주므로 페이징이나 중복 자료 정리에 사용합니다. 다만 동점일 때 누가 먼저인지는 정해져 있지 않으므로(1.7), 겹치지 않는 열을 ORDER BY 에 함께 적어야 매번 같은 결과가 나옵니다.

상세 사용법

WHERE 에서는 사용할 수 없습니다

"조회수 1·2위만 보고 싶다" 를 그대로 적으면 오류입니다.

SQL
SELECT num, hit_count FROM Board.POSTS
WHERE ROW_NUMBER() OVER (ORDER BY hit_count DESC) <= 3;
오류
메시지 4108 Windowed functions can only appear in the SELECT or ORDER BY clauses.

2.3 의 실행 차례를 떠올리십시오. WHERESELECT 보다 먼저 실행되는데, 윈도 함수는 SELECT 단계에서 계산됩니다. 그 시점에 아직 순위가 없습니다.

CTE 나 파생 테이블로 한 겹 감싸면 됩니다. 안쪽에서 순위를 매기고 바깥에서 거릅니다.

최소 예제

그룹마다 상위 N — 대표 용법

PARTITION BYROW_NUMBER 를 CTE 로 감싸면 게시판마다 1·2위를 한 번에 뽑을 수 있습니다. TOP 으로는 할 수 없는 일입니다. 그쪽은 결과 전체에서 앞의 몇 개를 자를 뿐입니다(1.7).

SQL
WITH 순위매김 AS (
    SELECT board_num, num, title, hit_count,
           ROW_NUMBER() OVER (
               PARTITION BY board_num
               ORDER BY hit_count DESC, num
           ) AS 순위
    FROM Board.POSTS
)
SELECT board_num, 순위, num, title, hit_count
FROM 순위매김
WHERE 순위 <= 2
ORDER BY board_num, 순위;
결과
board_num순위numtitlehit_count
1170제약 조건 실무에서 겪은 일490
12140인덱스 예제 모음480
21214윈도 함수 질문드립니다498
2271외래 키 실무에서 겪은 일497
3169뷰 실무에서 겪은 일483
3268임시 테이블 실무에서 겪은 일476
(6개 행이 영향을 받음)

ORDER BYnum 을 덧붙인 것은 동점일 때 차례를 못 박기 위해서입니다. 이것이 없으면 같은 문장이 실행할 때마다 다른 글을 1위로 낼 수 있습니다.

상세 사용법

2.4 의 상관 서브쿼리를 대신하기

2.4 실습에서 "자기 게시판 평균보다 조회수가 높은 글" 을 상관 서브쿼리로 적었습니다. 그때는 행마다 안쪽 조회가 한 번씩 돌았습니다. 윈도 함수로는 한 번만 탐색합니다.

SQL
WITH T AS (
    SELECT num, board_num, hit_count,
           AVG(hit_count) OVER (PARTITION BY board_num) AS 게시판평균
    FROM Board.POSTS
)
SELECT COUNT(*) AS 평균초과 FROM T WHERE hit_count > 게시판평균;
결과
평균초과
215
(1개 행이 영향을 받음)

2.4 의 상관 서브쿼리와 같은 215 입니다. 결과가 같으면서 읽기도 쉽습니다. "게시판 평균을 옆에 붙이고, 그보다 큰 것을 센다" 로 읽힙니다.

상세 사용법

누적 합과 앞뒤 행

OVER 안에 ORDER BY 를 두고 범위를 적으면 여기까지의 합을 낼 수 있습니다.

SQL
SELECT TOP 5 num, hit_count,
       SUM(hit_count) OVER (
           ORDER BY num
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS 누적
FROM Board.POSTS
ORDER BY num;
결과
numhit_count누적
177
21421
32142
42870
535105
(5개 행이 영향을 받음)

UNBOUNDED PRECEDING맨 앞부터, CURRENT ROW지금 행까지입니다. 매출 누계나 잔액 계산에 사용합니다.

LAGLEAD앞뒤 행의 값을 가져옵니다. 값이 얼마나 늘고 줄었는지 볼 때 사용합니다.

SQL
SELECT TOP 5 num, hit_count,
       LAG(hit_count)  OVER (ORDER BY num) AS 이전글,
       LEAD(hit_count) OVER (ORDER BY num) AS 다음글
FROM Board.POSTS
ORDER BY num;
결과
numhit_count이전글다음글
17NULL14
214721
3211428
4282135
5352842
(5개 행이 영향을 받음)

첫 행에는 앞 행이 없어 NULL 입니다. 뺄셈에 그대로 넣으면 결과도 NULL 이 되므로(1.9), COALESCE 로 채우거나 그 행을 걸러 내야 합니다.

실습 문제

직접 해보기

1. 회원마다 자기 글 순위 난이도 하

회원마다 자기가 작성한 글을 조회수 높은 차례로 번호를 매겨, 각자의 1위 글만 가져오세요.

WITH R AS ( SELECT P.user_num, P.num, P.title, P.hit_count, ROW_NUMBER() OVER ( PARTITION BY P.user_num ORDER BY P.hit_count DESC, P.num ) AS 순위 FROM Board.POSTS AS P ) SELECT TOP 5 U.nickname, R.title, R.hit_count FROM R INNER JOIN Member.USERS AS U ON U.num = R.user_num WHERE R.순위 = 1 ORDER BY R.hit_count DESC; -- 20 행이 나옵니다. 회원이 스무 명이기 때문입니다.
2. 앞 글과의 차이 난이도 중

자유게시판 글을 번호순으로 늘어놓고, 바로 앞 글보다 조회수가 얼마나 늘었는지를 함께 보이게 해보세요. 첫 글은 0 으로 나오게 하십시오.

LAG 의 두 번째·세 번째 인자로 몇 칸 앞인지와 없을 때 사용할 값을 줄 수 있습니다.
SELECT TOP 5 num, hit_count, hit_count - LAG(hit_count, 1, hit_count) OVER (ORDER BY num) AS 증감 FROM Board.POSTS WHERE board_num = 2 ORDER BY num; -- LAG(열, 1, 기본값) 으로 적으면 첫 행에서 NULL 대신 -- 기본값이 들어갑니다. 자기 값을 넣었으므로 차이가 0 이 됩니다. -- COALESCE 로 감싸도 됩니다.
요약
  • 윈도 함수는 행을 줄이지 않고 집계값을 옆에 붙입니다. GROUP BY 의 제약이 없습니다.
  • PARTITION BY 로 무리를 나눕니다. 나누기만 하고 줄이지는 않습니다.
  • 순위 셋은 동점을 다루는 법이 다릅니다(1·2·3 / 1·1·3 / 1·1·2).
  • WHERE 에서 사용할 수 없습니다(오류 4108). CTE 로 감싸 바깥에서 거르십시오.
  • PARTITION BY + ROW_NUMBER + CTE 가 그룹마다 상위 N 을 뽑는 정석입니다.
  • 상관 서브쿼리를 대신하면 같은 결과를 한 번의 탐색으로 얻습니다.