문서 읽는 데 44분 · D4

D-4: 윈도우 함수 ① — 행을 그대로 두고 옆에 순위를 붙이기

목차 21
전체 24강 중 16강 · 데이터베이스
난이도 · 입문

ℹ️Oracle SQL로 SQLD 자격증을 대비하며 실전 SQL을 쌓는 트랙이에요. 프로그래밍 경험이 없어도 시작할 수 있고, 부트캠프에서는 자바 기초 다음·스프링 부트 직전에 배치하길 권해요.

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 같은 표가 자기 자신을 부모로 가리키는 트리를, START WITHCONNECT BY 로 위에서 아래로 끝까지 펼치는 계층형 질의를 배웠어요. 트리를 세로로(부모-자식 깊이) 파고든 거죠.

오늘은 방향을 90도 틀어요. 데이터를 가로로 줄 세워 등수를 매깁니다. "좋아요를 가장 많이 받은 게시물 1·2·3등" 처럼요. 인스타그램을 떠올려 보세요. 인기 게시물 순위, 이번 달 팔로워가 지난달보다 몇 명 늘었나, 내 게시물 중에 어떤 게 제일 반응이 좋았나. 이런 "줄 세우기" 와 "앞뒤 비교" 를 한 방에 해 주는 게 바로 윈도우 함수예요.

지난 시간 끝에 제가 한 가지를 흘려 뒀어요. 집계가 여러 행을 한 값으로 뭉친다면, 윈도우 함수는 각 행을 그대로 둔 채 그 옆에 순위나 값을 붙여 준다고요. 오늘 그 차이를 확실히 풀어 볼게요. C-4 에서 배운 COUNT·SUM 같은 집계와 나란히 놓고 보면 정체가 금방 드러나요.

텍스트
 오늘의 여정
   ① 윈도우 함수란 — 집계는 뭉치고, 윈도우는 붙인다
   ② ROW_NUMBER — 줄 세워 1, 2, 3 유일 번호
   ③ RANK vs DENSE_RANK — 동점을 어떻게 셀까
   ④ NTILE — N 등분으로 나누기 (상위 25%)
   ⑤ PARTITION BY — 그룹 안에서 순위를 다시 1번부터
   ⑥ LAG / LEAD — 앞뒤 행을 끌어와 비교하기
   ⑦ FIRST_VALUE / LAST_VALUE — 구간의 처음과 끝 값
   ⑧ 마무리 — 윈도우 함수는 WHERE 에 못 쓴다

💡 오늘 수업의 핵심 — "집계가 여러 행을 한 값으로 뭉친다면, 윈도우 함수는 각 행을 그대로 둔 채 그 옆에 순위·증감·구간 값을 붙인다. 그 비밀은 OVER() 한 조각에 있다"

🎯 학습 목표

  • 집계 함수와 윈도우 함수의 결정적 차이(행을 뭉치느냐, 그대로 두느냐)를 이해하고 OVER() 절의 구조를 읽는다. (SQLD 2과목 'SQL 활용 — 윈도우 함수', ★빈출)
  • ROW_NUMBER·RANK·DENSE_RANK·NTILE 로 순위를 매기고, 동점을 함수마다 어떻게 다르게 처리하는지 구분한다.
  • PARTITION BY 로 그룹 안에서 순위를 다시 매기고, LAG·LEAD·FIRST_VALUE·LAST_VALUE 로 앞뒤 행과 구간의 값을 끌어와 비교한다.

Step 1: "윈도우 함수란 무엇인가 — 집계는 뭉치고, 윈도우는 붙인다"

윈도우 함수의 정체를 잡으려면, 지난 C-4 에서 배운 집계 함수를 다시 꺼내는 게 가장 빨라요. 그때 우리는 GROUP BY member_id 로 회원별 게시물 수를 셌죠. 게시물 50건이 회원 9그룹으로 뭉쳐, 결과가 9줄로 줄어들었어요. COUNT·SUM·AVG 같은 집계 함수는 이렇게 여러 행을 한 값으로 뭉치는 게 본업이에요.

그런데 실무에선 이런 요구가 자주 와요. "게시물 목록은 그대로 다 보여주되, 각 게시물 옆에 좋아요 순위를 같이 붙여 줘." 집계로는 곤란해요. 집계는 행을 뭉쳐 버리니까, 50줄을 그대로 두면서 순위만 옆에 붙이는 게 안 되거든요. 바로 이럴 때 윈도우 함수가 나서요.

텍스트
 집계 함수 (C-4) — 여러 행을 "한 값으로 뭉친다"
   like: 6, 5, 4, 3, 2 ...   ──COUNT()──►   13      (결과 한 줄)

 윈도우 함수 — 각 행을 "그대로 둔 채 옆 칸을 새로 붙인다"
   like │ rank          좋아요 13줄이 13줄 그대로 남고,
     6  │  1            그 옆에 순위(rank) 칸이 새로 생긴다
     5  │  2
     4  │  3
    ... │ ...

핵심은 행 수예요. 집계는 N 줄을 1 줄로 줄이고, 윈도우는 N 줄을 N 줄 그대로 둬요. 그러면서 옆에 새 컬럼(순위·합계·앞 행 값 등)을 하나 더 만들어 주는 거죠. 이 "행을 유지한 채 계산" 을 가능하게 하는 문법이 OVER() 예요.

텍스트
 RANK() OVER (ORDER BY like_cnt DESC)
 └──┬──┘ └──┬──────────────────────┘
  어떤 값을      어떤 창(window)을 보고 계산할지
  계산할지        ORDER BY = 무슨 기준으로 줄 세울지

OVER() 는 "이 함수가 들여다볼 창(window)" 을 정하는 부분이에요. 윈도우 함수라는 이름도 여기서 왔어요. 오늘은 이 창을 ORDER BY(줄 세우는 기준)와 PARTITION BY(그룹 나누기) 두 가지로 조절하는 법을 배웁니다.

💡 한 줄 정리 — 집계 함수는 여러 행을 한 값으로 뭉치고, 윈도우 함수는 각 행을 그대로 둔 채 OVER() 가 정한 창을 보고 계산해 옆 칸에 값을 붙인다.

🙋 학생 질문 — "튜터님, 그럼 GROUP BY 랑 윈도우 함수는 뭐가 다른 거예요?"

딱 한 가지, 결과의 행 수가 달라요. GROUP BY member_id 로 집계하면 회원 한 명당 한 줄, 9줄로 줄어들어요. 원래 게시물이 몇 줄이었는지는 사라지죠. 반면 윈도우 함수는 게시물 50줄을 그대로 50줄로 두고, 각 줄 옆에 "이 회원의 게시물 수" 든 "전체 순위" 든 계산한 값을 덧붙여요. 그래서 "원본 행은 다 보여주면서 + 계산값도 같이" 가 필요할 때 윈도우 함수를 써요. 뭉쳐야 할 땐 GROUP BY, 그대로 두고 옆에 붙여야 할 땐 OVER() 라고 기억하면 돼요.


Step 2: "줄 세워 번호 매기기 — ROW_NUMBER"

가장 단순한 순위 함수부터 가요. ROW_NUMBER 는 정해진 순서대로 1, 2, 3… 번호를 매겨요. 게시물별 좋아요 수를 세서, 좋아요가 많은 순으로 줄을 세워 볼게요.

SQL
-- sql/queries/D4_window.sql
SELECT p.post_id, m.nickname, like_cnt,
       ROW_NUMBER() OVER (ORDER BY like_cnt DESC, p.post_id) AS rn
FROM   post p
JOIN   member m ON m.member_id = p.member_id
JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
       ON pl.post_id = p.post_id
ORDER BY rn;

안쪽 (SELECT post_id, COUNT(*) ... GROUP BY post_id) 가 게시물별 좋아요 수를 먼저 집계해요(C-4 에서 배운 그 집계예요). 그 결과를 post·member 와 이어서, 바깥에서 ROW_NUMBER() OVER (ORDER BY like_cnt DESC) 로 좋아요 많은 순서대로 번호를 붙이죠. 좋아요가 같을 때 순서가 뒤죽박죽되지 않게 p.post_id 를 정렬 기준에 하나 더 얹었어요.

좋아요를 한 번이라도 받은 게시물은 13개예요. 결과는 이렇게 나와요.

post_id nickname like_cnt rn
12 강지수 6 1
1 김재훈 5 2
40 김유나 4 3
5 이민지 3 4
18 박지훈 3 5
13 김유나 2 6
22 이민지 2 7
39 강지수 2 8
2 김재훈 1 9
9 정하루 1 10
16 한소라 1 11
28 최도연 1 12
50 박지훈 1 13

ROW_NUMBER 의 성격이 여기서 드러나요. 4등(post 5)과 5등(post 18)은 좋아요가 똑같이 3개인데도 번호는 4와 5로 갈려요. 좋아요 1개짜리 다섯 게시물(9·10·11·12·13등)도 마찬가지로 전부 다른 번호를 받았죠. ROW_NUMBER 는 동점이든 아니든 무조건 1부터 끝까지 겹치지 않는 유일한 번호를 매겨요. 그래서 "줄 세우기" 라는 말이 가장 잘 어울리는 함수예요.

💡 한 줄 정리ROW_NUMBER() OVER (ORDER BY ...) 는 정렬 순서대로 1부터 겹치지 않는 유일한 번호를 매긴다. 동점이어도 번호가 갈린다.


Step 3: "동점을 어떻게 셀까 — RANK vs DENSE_RANK" ★빈출

방금 본 동점, 좋아요 3개짜리 두 게시물을 4·5등으로 갈라 버린 게 마음에 걸리지 않나요? 현실의 등수는 보통 "공동 4등" 이라고 부르잖아요. 이걸 표현하는 함수가 RANKDENSE_RANK 예요. 둘 다 동점에는 같은 등수를 주는데, 동점 다음 등수를 매기는 방식이 달라요. 이 차이가 SQLD 단골 함정이라 오늘의 꽃이에요.

SQL
-- sql/queries/D4_window.sql
SELECT p.post_id, like_cnt,
       RANK()       OVER (ORDER BY like_cnt DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY like_cnt DESC) AS drnk
FROM   post p
JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
       ON pl.post_id = p.post_id
ORDER BY like_cnt DESC, p.post_id;
post_id like_cnt RANK DENSE_RANK
12 6 1 1
1 5 2 2
40 4 3 3
5 3 4 4
18 3 4 4
13 2 6 5
22 2 6 5
39 2 6 5
2 1 9 6
9 1 9 6
16 1 9 6
28 1 9 6
50 1 9 6

좋아요 3개짜리 두 게시물은 둘 다 공동 4등이에요. 여기까진 RANKDENSE_RANK 가 똑같아요. 차이는 그 다음부터예요.

좋아요 2개짜리(13·22·39)를 보세요. RANK 는 6등을 줘요. 앞에 4등이 두 명 있었으니 5등을 건너뛰고 6등으로 점프하는 거예요. 반면 DENSE_RANK 는 5등을 줘요. 동점이 몇 명이든 다음 등수는 바로 이어 붙이거든요. 좋아요 1개짜리에서 차이가 더 벌어져요. RANK 는 9등(앞의 6등 셋을 건너뛰고 7·8을 비움), DENSE_RANK 는 6등이에요.

텍스트
 동점(공동 4등) 다음 등수를 어떻게?
   RANK       : 1  2  3  4  4  6  6  6  9  9 ...    동점 수만큼 건너뛴다 (5·7·8 비움)
   DENSE_RANK : 1  2  3  4  4  5  5  5  6  6 ...    건너뛰지 않고 바로 이어 붙인다

올림픽 메달을 떠올리면 쉬워요. 금메달이 둘이면 은메달 없이 바로 동메달(3위)로 가죠? 그게 RANK 예요. 반면 "난이도 등급" 처럼 빈 등급 없이 촘촘히 매기고 싶으면 DENSE_RANK 고요. 시험에선 "공동 4등이 둘일 때 다음은 몇 등인가?" 를 물어요. RANK 면 6등, DENSE_RANK 면 5등. 이 한 줄만 확실히 잡으면 돼요.

💡 한 줄 정리RANKDENSE_RANK 는 동점에 같은 등수를 주지만, 다음 등수에서 RANK 는 동점 수만큼 건너뛰고(4,4,6) DENSE_RANK 는 건너뛰지 않는다(4,4,5).

🙋 학생 질문 — "튜터님, 그럼 셋 중에 실무에선 뭘 제일 많이 써요?"

상황에 따라 갈려요. "공동 순위를 인정하는 진짜 등수표"(예: 좋아요 랭킹 화면)에는 RANK 가 자연스러워요. 사람들이 "공동 4위" 를 기대하니까요. "등급을 빈칸 없이 촘촘히 나누고 싶다"(예: 1군·2군·3군) 면 DENSE_RANK. 그리고 "동점이어도 무조건 한 명만 뽑아야 한다"(예: 선착순 한 명, 페이지네이션) 면 동점을 허용하지 않는 ROW_NUMBER 를 써요. 동점을 어떻게 다룰지가 함수 선택의 기준이에요.


Step 4: "N 등분으로 나누기 — NTILE"

순위를 한 명 한 명 매기는 대신, "상위 25%·그다음 25%…" 처럼 덩어리로 나누고 싶을 때가 있어요. 인기 게시물을 상·중·하로 나누거나, 회원을 활동량 기준 4분위로 나누는 식이죠. 이걸 해 주는 게 NTILE(N) 이에요. 정렬한 행을 N 개의 그룹으로 최대한 고르게 잘라요.

좋아요 받은 게시물 13개를 4분위로 나눠 볼게요.

SQL
-- sql/queries/D4_window.sql
SELECT p.post_id, like_cnt,
       NTILE(4) OVER (ORDER BY like_cnt DESC, p.post_id) AS quartile
FROM   post p
JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
       ON pl.post_id = p.post_id
ORDER BY like_cnt DESC, p.post_id;
post_id like_cnt quartile
12 6 1
1 5 1
40 4 1
5 3 1
18 3 2
13 2 2
22 2 2
39 2 3
2 1 3
9 1 3
16 1 4
28 1 4
50 1 4

여기서 재미있는 점이 보여요. 13개를 4그룹으로 나누면 딱 떨어지지 않죠? 13 ÷ 4 = 3 이고 1이 남아요. 이때 NTILE 은 남는 1개를 앞 그룹부터 하나씩 더 얹어요. 그래서 1분위만 4개, 나머지 2·3·4분위는 3개씩 나뉘었어요.

텍스트
 13개를 4분위로: 3개씩 나누면 1개가 남는다  남는 1개는 앞 분위부터
   1분위 = 4개   (3개 + 남은 1개)
   2분위 = 3개
   3분위 = 3개
   4분위 = 3개

⚠️ 주의할 점은 NTILE 이 값이 같든 다르든 정해진 개수로 칼같이 자른다는 거예요. 좋아요 2개짜리 세 게시물(13·22·39)을 보세요. 같은 좋아요 수인데도 13·22는 2분위, 39는 3분위로 갈려요. RANK 라면 셋 다 공동 6등을 줬을 텐데, NTILE 은 동점을 신경 쓰지 않고 "그룹 크기" 만 맞춰요. 그래서 분위를 나눌 땐 경계에 걸친 동점이 어디로 갈지 미리 알아 두는 게 좋아요.

💡 한 줄 정리NTILE(N) 은 정렬한 행을 N 개의 고른 그룹으로 자른다. 딱 안 나뉘면 앞 그룹부터 한 개씩 더 채우고, 동점이어도 그룹 크기를 우선해 자른다.


Step 5: "그룹 안에서 다시 1번부터 — PARTITION BY" ★빈출

지금까진 게시물 전체를 한 줄로 세웠어요. 그런데 "회원별로 각자 자기 게시물 중에 몇 등인지" 처럼, 그룹을 나눠 그 안에서 따로 순위를 매기고 싶을 때가 있어요. 이때 쓰는 게 PARTITION BY 예요.

PARTITION BY 는 창(window)을 그룹별로 쪼개요. 그룹이 바뀌면 순위가 다시 1번부터 시작하죠. 회원별로 자기 게시물 중 좋아요 순위를 매겨 볼게요.

SQL
-- sql/queries/D4_window.sql
SELECT m.nickname, p.post_id, like_cnt,
       RANK() OVER (PARTITION BY p.member_id ORDER BY like_cnt DESC) AS rnk_in_member
FROM   post p
JOIN   member m ON m.member_id = p.member_id
JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
       ON pl.post_id = p.post_id
ORDER BY p.member_id, rnk_in_member;

OVER (PARTITION BY p.member_id ORDER BY like_cnt DESC) 는 "회원별로 나눈 다음, 각 회원 안에서 좋아요 많은 순으로 등수를 매겨라" 는 뜻이에요. 결과를 보세요.

nickname post_id like_cnt rnk_in_member
김재훈 1 5 1
김재훈 2 1 2
이민지 5 3 1
이민지 22 2 2
정하루 9 1 1
최도연 28 1 1
강지수 12 6 1
강지수 39 2 2
김유나 40 4 1
김유나 13 2 2
한소라 16 1 1
박지훈 18 3 1
박지훈 50 1 2

회원이 바뀔 때마다 등수가 1로 리셋되는 게 보이죠? 김재훈의 1등은 좋아요 5개(post 1)지만, 강지수의 1등은 좋아요 6개(post 12)예요. 등수의 기준이 "전체" 가 아니라 "그 회원 안" 으로 바뀐 거예요. 게시물이 하나뿐인 정하루·최도연·한소라는 자기 안에서 무조건 1등이고요.

여기서 C-4 의 GROUP BY 와 헷갈리기 쉬워요. ⚠️ GROUP BY member_id 는 회원당 한 줄로 뭉쳐 9줄이 됐지만, PARTITION BY member_id 는 게시물 13줄을 그대로 두면서 회원 그룹 안에서 순위만 매겨요. 같은 "회원별로" 라도 한쪽은 뭉치고 한쪽은 그대로 두는 거예요. 이게 Step 1 에서 말한 그 차이예요.

💡 한 줄 정리PARTITION BY 는 창을 그룹별로 쪼개 그룹이 바뀌면 순위를 1부터 다시 매긴다. GROUP BY 처럼 그룹을 나누지만 행을 뭉치지는 않는다.


Step 6: "앞뒤 행을 끌어와 비교하기 — LAG / LEAD"

순위 말고 윈도우 함수의 또 다른 큰 쓰임이 "앞뒤 행 비교" 예요. "이번 달 팔로워가 지난달보다 몇 명 늘었나" 같은 추이 분석이죠. 평범한 SQL 은 한 행을 계산할 때 그 행만 봐요. 옆줄(앞 행·뒤 행)을 가져올 방법이 마땅찮죠. LAG(앞 행)와 LEAD(뒤 행)가 바로 이걸 해 줘요.

여기서 새 데이터가 하나 필요해요. 우리 follow 표는 "누가 누구를 언제 팔로우했나" 라는 사건만 기록하는데, 그 사건이 전부 5월에 몰려 있어요. "전월 대비 증감" 같은 기간별 추이는 안 보이죠. 그래서 인스타 인사이트처럼 매월 1일에 팔로워 수를 찍어 적재하는 월별 지표 표 account_monthly_stat 를 따로 뒀어요. 회원 네 명(김재훈·강지수·김유나·박지훈)의 1월부터 6월까지 팔로워 수가 들어 있어요.

SQL
-- sql/queries/D4_window.sql — 김재훈(member 1)의 월별 팔로워 추이
SELECT m.nickname, TO_CHAR(s.stat_month, 'YYYY-MM') AS mon, s.follower_count,
       LAG(s.follower_count)  OVER (PARTITION BY s.member_id ORDER BY s.stat_month) AS prev_mon,
       s.follower_count
         - LAG(s.follower_count) OVER (PARTITION BY s.member_id ORDER BY s.stat_month) AS diff,
       LEAD(s.follower_count) OVER (PARTITION BY s.member_id ORDER BY s.stat_month) AS next_mon
FROM   account_monthly_stat s
JOIN   member m ON m.member_id = s.member_id
WHERE  s.member_id = 1
ORDER BY s.stat_month;

LAG(follower_count) OVER (ORDER BY stat_month) 는 "월 순서대로 줄 세웠을 때, 한 칸 앞(지난달) 행의 follower_count 를 가져와라" 예요. LEAD 는 반대로 한 칸 뒤(다음 달)고요. 그러면 이번 달 값에서 지난달 값을 빼서 증감(diff)을 바로 구할 수 있죠.

nickname mon follower_count prev_mon diff next_mon
김재훈 2026-01 100 108
김재훈 2026-02 108 100 +8 120
김재훈 2026-03 120 108 +12 110
김재훈 2026-04 110 120 -10 130
김재훈 2026-05 130 110 +20 145
김재훈 2026-06 145 130 +15

두 가지를 짚을게요. 첫째, ⚠️ 맨 첫 행(1월)의 prev_mon 이 비어 있어요(NULL). 1월은 그 앞에 가져올 지난달이 없으니까요. 그래서 diff 도 NULL 이 되죠. 마찬가지로 맨 끝 행(6월)은 next_mon 이 NULL 이에요. 앞뒤 비교에서 양 끝이 NULL 이 되는 건 자연스러운 일이라, 실무에선 NVL 로 0 처럼 채워 주기도 해요(C-3 에서 배운 그 NVL 이에요).

둘째, 4월을 보세요. 지난달 120 에서 110 으로 줄어 diff-10 이에요. 늘어난 달뿐 아니라 줄어든 달도 한눈에 잡히죠. 다른 회원에도 이렇게 빠진 달이 숨어 있어요. 강지수는 5월에 -5, 김유나는 3월에 -25 만큼 팔로워가 줄었어요. PARTITION BY s.member_id 덕분에 회원이 바뀌면 비교도 그 회원 안에서만 이뤄져서, 김재훈의 6월 다음에 강지수의 1월을 잘못 끌어오는 일은 없어요.

💡 한 줄 정리LAG(앞 행)·LEAD(뒤 행)는 정렬한 창에서 옆줄 값을 끌어와 같은 행에 붙인다. 양 끝은 가져올 값이 없어 NULL 이 되고, PARTITION BY 로 그룹 안에서만 비교한다.


Step 7: "구간의 처음과 끝 값 — FIRST_VALUE / LAST_VALUE"

LAG·LEAD 가 "바로 옆 한 칸" 을 본다면, "이 구간의 맨 처음 값" 이나 "맨 끝 값" 을 통째로 가져오고 싶을 때도 있어요. 예를 들어 "이 회원의 1월(첫 달) 팔로워가 몇 명이었지?" 를 모든 행 옆에 붙여, 지금이 출발점보다 얼마나 컸는지 한눈에 보는 거죠. 이걸 FIRST_VALUE 가 해 줘요.

강지수의 월별 팔로워로 보면서, 마지막 값까지 같이 구해 볼게요.

SQL
-- sql/queries/D4_window.sql — 강지수(member 6)
SELECT TO_CHAR(s.stat_month, 'YYYY-MM') AS mon, s.follower_count,
       FIRST_VALUE(s.follower_count) OVER (PARTITION BY s.member_id ORDER BY s.stat_month)      AS first_mon,
       FIRST_VALUE(s.follower_count) OVER (PARTITION BY s.member_id ORDER BY s.stat_month DESC) AS last_mon,
       LAST_VALUE(s.follower_count)  OVER (PARTITION BY s.member_id ORDER BY s.stat_month)      AS last_value_trap
FROM   account_monthly_stat s
WHERE  s.member_id = 6
ORDER BY s.stat_month;
mon follower_count first_mon last_mon last_value_trap
2026-01 200 200 290 200
2026-02 215 200 290 215
2026-03 240 200 290 240
2026-04 260 200 290 260
2026-05 255 200 290 255
2026-06 290 200 290 290

first_mon 을 보세요. 모든 행에 1월 값 200 이 똑같이 붙었어요. FIRST_VALUE(... ORDER BY stat_month) 가 "이 구간의 첫 행 값" 을 가져온 거죠. 강지수는 200 명에서 출발했다는 걸 어느 달에서 봐도 알 수 있어요.

그럼 "마지막 값" 은요? 정렬을 거꾸로(ORDER BY stat_month DESC) 뒤집어서 FIRST_VALUE 를 쓰면, 거꾸로 본 첫 행 = 원래의 마지막 행이라 6월 값 290 이 깔끔하게 붙어요(last_mon). 이게 가장 안전한 방법이에요.

그런데 맨 오른쪽 last_value_trap 을 보세요. 분명 LAST_VALUE 를 썼는데 마지막 값 290 이 아니라 그 행의 현재 값(200·215·240…)이 나와요. ⚠️ 이게 LAST_VALUE 의 유명한 함정이에요. 윈도우 함수는 정렬했을 때 "어디까지를 한 창으로 볼지" 범위를 기본값으로 "맨 앞부터 지금 행까지" 로 잡아요. 그래서 LAST_VALUE(창의 마지막 값)가 늘 "지금 행" 이 돼 버리는 거예요.

이 범위(프레임)를 직접 조절해서 LAST_VALUE 가 진짜 마지막 행을 보게 만드는 방법이 있는데, 그건 다음 시간에 누적·프레임을 다루면서 제대로 풀어요. 오늘은 "마지막 값이 필요하면 FIRST_VALUE 를 거꾸로 정렬해서 쓴다" 만 기억해 두면 충분해요.

💡 한 줄 정리FIRST_VALUE 는 구간의 첫 값을 모든 행에 붙인다. "마지막 값" 은 정렬을 뒤집어 FIRST_VALUE 로 얻는 게 안전하다. LAST_VALUE 는 기본 범위 탓에 현재 행 값이 나오는 함정이 있다.


Step 8: "마무리 — 윈도우 함수는 WHERE 에 직접 못 쓴다" ★빈출

마지막으로 윈도우 함수를 쓸 때 누구나 한 번은 부딪히는 벽을 짚고 갈게요. "좋아요 상위 3등만 보고 싶다" 며 이렇게 적으면 어떻게 될까요?

SQL
-- 이렇게 적으면 오류가 난다
SELECT p.post_id
FROM   post p
JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
       ON pl.post_id = p.post_id
WHERE  RANK() OVER (ORDER BY like_cnt DESC) <= 3;

실행하면 ORA-30483: window functions are not allowed here 오류가 나요. 윈도우 함수를 WHERE 절에 직접 쓸 수 없다는 뜻이에요. 왜일까요? SQL 은 WHERE 로 행을 먼저 걸러낸 다음, 살아남은 행을 가지고 SELECT 단계에서 순위를 계산해요. 그런데 WHERE 시점엔 아직 순위가 만들어지기 전이라, 거기서 순위를 조건으로 쓰는 건 "아직 굽지도 않은 빵을 자르려는" 셈이 되는 거예요.

텍스트
 SQL 처리 순서 (C-4 에서 본 그 순서)
   FROM  WHERE  GROUP BY  SELECT  ORDER BY
   · WHERE 단계  : 아직 순위가 없다  그래서 여기선 못 쓴다
   · SELECT 단계 : 이때 윈도우 함수가 계산된다  순위가 생긴다

해법은 간단해요. 순위를 먼저 만든 쿼리를 인라인 뷰(서브쿼리)로 감싸고, 그 바깥에서 거르는 거예요. C-4 에서 HAVING 으로 집계 결과를 거르고, D-2 에서 인라인 뷰를 배운 게 여기서 이어져요.

SQL
-- sql/queries/D4_window.sql — 순위를 먼저 만든 뒤, 바깥에서 거른다
SELECT post_id, like_cnt, rnk
FROM (
  SELECT p.post_id, like_cnt,
         RANK() OVER (ORDER BY like_cnt DESC) AS rnk
  FROM   post p
  JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
         ON pl.post_id = p.post_id
)
WHERE rnk <= 3
ORDER BY rnk, post_id;

안쪽에서 rnk 라는 순위 컬럼을 먼저 완성하고, 바깥에서 WHERE rnk <= 3 으로 거르니 잘 돌아가요.

post_id like_cnt rnk
12 6 1
1 5 2
40 4 3

💡 한 줄 정리 — 윈도우 함수는 SELECT 단계에서 계산돼 WHERE 에 직접 못 쓴다(ORA-30483). 순위로 거르려면 인라인 뷰로 감싸 바깥에서 조건을 건다.


마무리

오늘은 데이터를 가로로 줄 세워 순위를 매기고, 앞뒤 행을 끌어와 비교하는 윈도우 함수를 배웠어요. 집계가 행을 뭉친다면 윈도우는 행을 그대로 둔 채 옆 칸에 값을 붙인다는 차이에서 출발해, OVER() 절 하나로 순위·증감·구간 값을 만들어 냈죠.

오늘 배운 핵심 정리

  • 💡 집계 vs 윈도우 — 집계 함수는 여러 행을 한 값으로 뭉치고, 윈도우 함수는 각 행을 그대로 둔 채 OVER() 가 정한 창을 보고 계산해 옆 칸에 붙인다.
  • 💡 순위 4총사ROW_NUMBER(무조건 유일 번호), RANK(동점 뒤 건너뜀: 4,4,6), DENSE_RANK(안 건너뜀: 4,4,5), NTILE(N 등분). 동점을 어떻게 다루느냐로 갈린다.
  • 💡 그룹과 비교PARTITION BY 로 그룹 안에서 순위를 리셋하고, LAG·LEAD 로 앞뒤 행을, FIRST_VALUE 로 구간의 첫(거꾸로 정렬하면 마지막) 값을 끌어온다.
  • 💡 윈도우의 위치 — 윈도우 함수는 SELECT 단계에서 계산되므로 WHERE 에 직접 못 쓰고, 인라인 뷰로 감싸 바깥에서 거른다.

다음 시간 예고

오늘 우리는 순위를 매기고 옆 행을 가져오는 "순위·조회" 까지 했어요. 다음 시간엔 같은 OVER() 를 한 단계 더 밀어붙여요. "1월부터 이번 달까지 팔로워를 차곡차곡 더한 누적 합계" 처럼, 창의 범위를 직접 조절하는 윈도우 프레임(ROWS BETWEEN)을 배웁니다. 오늘 풀다 만 LAST_VALUE 의 함정도 이 프레임으로 깔끔히 해결하고요. 거기에 소계와 총계를 한 번에 뽑아 주는 ROLLUP·CUBE 까지 더해, 보고서용 집계의 끝을 봅니다. 윈도우 함수의 진짜 위력이 다음 시간에 터져요.


과제

오늘 배운 윈도우 함수를 직접 써 보는 과제예요. 결과가 몇 줄 나올지, 동점은 어떻게 처리될지 먼저 머릿속으로 그려 본 뒤 실행해 맞는지 확인해 보세요.

[기초] 순위 4총사 비교

게시물별 좋아요 수를 구한 뒤(post_likeGROUP BY post_id 로 집계), 좋아요 많은 순으로 다음을 한 줄에 모두 붙여 보세요. (가) ROW_NUMBER·RANK·DENSE_RANK 를 같은 ORDER BY like_cnt DESC 로 나란히 출력하고, 좋아요 2개짜리 세 게시물에서 세 함수의 값이 각각 어떻게 다른지 적어 보세요. (나) NTILE(3) 을 더해 13개 게시물을 3분위로 나눠, 어느 분위가 몇 개씩 나뉘는지 확인해 보세요.

[응용] PARTITION BY 로 월별 순위 매기기

account_monthly_stat 표로 다음을 해 보세요. (가) 같은 달끼리 묶어(PARTITION BY stat_month) 팔로워가 많은 순으로 RANK 를 매겨, 매달 1등이 누구인지 보세요. (나) 4월의 순위를 1·2·3월과 비교해, 순위가 뒤집힌 회원 쌍이 있는지 찾아보세요. (다) PARTITION BY 를 빼면 결과가 어떻게 달라지는지 생각해 보고, 실제로 빼서 확인해 보세요.

[심화] 증감과 인라인 뷰

account_monthly_stat 표로 다음을 해 보세요. (가) 회원별로 LAG 를 써서 전월 대비 팔로워 증감(diff)을 구하고, diff 가 음수인(팔로워가 줄어든) 행만 골라 보세요. 윈도우 함수를 바로 WHERE 에 못 쓴다는 점을 떠올리면서요. (나) FIRST_VALUE 로 각 회원의 1월(첫 달) 팔로워를 모든 행에 붙이고, 6월 팔로워가 1월 대비 가장 많이 증가한 회원이 누구인지 찾아보세요.


생각해볼 주제

1. 동점을 인정할 것인가, 한 줄로 자를 것인가

같은 좋아요 수를 받은 게시물이 여럿일 때, RANK 는 공동 순위를 인정하고 ROW_NUMBER 는 동점이어도 억지로 한 줄로 갈라요. "좋아요 랭킹 화면" 과 "선착순 한 명만 뽑는 이벤트" 는 동점을 대하는 태도가 정반대죠. 내가 만드는 기능에서 동점을 인정해야 하는지, 아니면 어떤 기준으로든 단 하나를 골라야 하는지를 어떻게 판단할지 생각해 보세요.

2. 집계로 풀 수 있는 걸 왜 윈도우로 풀까

회원별 게시물 수 같은 건 C-4 의 GROUP BY 로도 구할 수 있어요. 그런데 "게시물 목록은 다 보여주면서 각 줄 옆에 그 회원의 게시물 수도 같이" 가 필요하면 이야기가 달라지죠. GROUP BY 로 뭉친 결과를 다시 원본과 조인하는 방법과, 윈도우 함수로 한 번에 붙이는 방법을 놓고, 각각 어떤 상황에서 더 읽기 좋고 빠른지 견줘 보세요.

3. 순위 계산을 DB 에서 할까, 애플리케이션에서 할까

좋아요 랭킹은 오늘처럼 데이터베이스가 RANK 로 매겨 줄 수도 있고, 게시물 목록만 받아 와 애플리케이션 코드가 정렬하며 순위를 매길 수도 있어요. 데이터가 수십만 건이라면, 또는 순위를 실시간으로 자주 갱신해야 한다면 어느 쪽이 유리할지, 데이터베이스 부하와 네트워크로 오가는 데이터 양을 기준으로 따져 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답 쿼리를 그대로 베끼기보다, "동점을 어떻게 처리하고 싶은가(RANK/DENSE_RANK/ROW_NUMBER)", "순위를 전체로 매길까 그룹 안에서 매길까(PARTITION BY)", "앞뒤 행을 어떻게 끌어올까(LAG/LEAD)" 를 떠올리는 감각을 기르는 게 목표예요.


🎯 [과제 1 예시답안] 순위 4총사 비교

채점 포인트

항목 배점 핵심
(가) 세 함수 나란히 + 동점 비교 60% 좋아요 2개 세 게시물에서 ROW_NUMBER 6·7·8 / RANK 6·6·6 / DENSE_RANK 5·5·5
(나) NTILE(3) 분배 40% 13개 → 5·4·4 (앞 분위부터 +1)

풀이 예시

(가) ROW_NUMBER · RANK · DENSE_RANK 나란히

게시물별 좋아요 수를 먼저 집계하고, 같은 ORDER BY like_cnt DESC 로 세 함수를 한 줄에 붙여요.

SQL
SELECT p.post_id, like_cnt,
       ROW_NUMBER() OVER (ORDER BY like_cnt DESC, p.post_id) AS rn,
       RANK()       OVER (ORDER BY like_cnt DESC)            AS rnk,
       DENSE_RANK() OVER (ORDER BY like_cnt DESC)            AS drnk
FROM   post p
JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
       ON pl.post_id = p.post_id
ORDER BY like_cnt DESC, p.post_id;

좋아요 2개짜리 세 게시물(13·22·39)만 떼어 보면 세 함수의 성격이 한눈에 갈려요.

post_id like_cnt ROW_NUMBER RANK DENSE_RANK
13 2 6 6 5
22 2 7 6 5
39 2 8 6 5

같은 좋아요 2개인데 ROW_NUMBER 는 6·7·8 로 억지로 갈라 버리고, RANK 는 셋 다 공동 6등, DENSE_RANK 는 셋 다 5등을 줘요. RANK 가 6등인 건 앞에 공동 4등이 둘 있어 5등을 건너뛰었기 때문이고, DENSE_RANK 는 건너뛰지 않아 바로 5등이죠.

(나) NTILE(3) 분배

SQL
SELECT tile, COUNT(*) AS cnt
FROM (
  SELECT NTILE(3) OVER (ORDER BY like_cnt DESC, p.post_id) AS tile
  FROM   post p
  JOIN   (SELECT post_id, COUNT(*) AS like_cnt FROM post_like GROUP BY post_id) pl
         ON pl.post_id = p.post_id
)
GROUP BY tile
ORDER BY tile;
tile cnt
1 5
2 4
3 4

13개를 3분위로 나누면 13 ÷ 3 = 4 에 1이 남아요. 남는 1개를 앞 분위부터 채우니 1분위만 5개, 2·3분위는 4개씩이에요. (Step 4 의 4분위 4·3·3·3 과 같은 원리예요.)

한 걸음 더

NTILE(3) 을 바로 GROUP BY 에 넣어 분배를 세려고 하면 ORA-30483 오류가 나요. 윈도우 함수는 SELECT 단계에서 계산돼 GROUP BY 시점엔 아직 없거든요. 그래서 위처럼 NTILE 으로 분위를 먼저 만든 쿼리를 인라인 뷰로 감싸고, 바깥에서 GROUP BY tile 로 세야 해요. Step 8 에서 본 "윈도우는 인라인 뷰로 감싼다" 가 여기서도 그대로 적용돼요.


🎯 [과제 2 예시답안] PARTITION BY 로 월별 순위 매기기

채점 포인트

항목 배점 핵심
(가) PARTITION BY stat_month 월별 1등 40% 강지수가 6개월 내내 1등
(나) 순위 역전 찾기 35% 4월에 박지훈이 김재훈을 추월(3등↔4등)
(다) PARTITION BY 제거 효과 25% 24행 전체를 한 줄로 줄 세움

풀이 예시

(가) 같은 달끼리 묶어 순위 매기기

SQL
SELECT TO_CHAR(stat_month, 'YYYY-MM') AS mon, m.nickname, follower_count,
       RANK() OVER (PARTITION BY stat_month ORDER BY follower_count DESC) AS rnk
FROM   account_monthly_stat s
JOIN   member m ON m.member_id = s.member_id
ORDER BY stat_month, rnk;

PARTITION BY stat_month 로 달이 바뀔 때마다 순위가 1부터 다시 매겨져요. 매달 1등을 추려 보면 이래요.

mon 1등 2등 3등 4등
2026-01 강지수 김유나 김재훈 박지훈
2026-02 강지수 김유나 김재훈 박지훈
2026-03 강지수 김유나 김재훈 박지훈
2026-04 강지수 김유나 박지훈 김재훈
2026-05 강지수 김유나 박지훈 김재훈
2026-06 강지수 김유나 박지훈 김재훈

강지수가 6개월 내내 1등이고, 김유나가 줄곧 2등이에요.

(나) 순위가 뒤집힌 회원 쌍

3등·4등을 보면 1~3월엔 김재훈이 3등, 박지훈이 4등이었는데, 4월부터 박지훈이 3등으로 올라서고 김재훈이 4등으로 내려가요. 4월에 박지훈(130명)이 김재훈(110명)을 추월한 거죠. 순위는 고정이 아니라 그달의 값에 따라 매번 다시 매겨진다는 걸 보여줘요.

(다) PARTITION BY 를 빼면

SQL
SELECT TO_CHAR(stat_month, 'YYYY-MM') AS mon, m.nickname, follower_count,
       RANK() OVER (ORDER BY follower_count DESC) AS rnk
FROM   account_monthly_stat s
JOIN   member m ON m.member_id = s.member_id
ORDER BY rnk;

PARTITION BY 가 사라지면 달 구분 없이 24행 전체를 한 줄로 줄 세워요. 그러면 강지수의 6월 290명이 전체 1등, 강지수의 5월 255명이 2등… 식으로, "어느 달이든 팔로워가 가장 많은 스냅샷" 순위가 나와요. 같은 RANK 라도 창을 그룹으로 쪼개느냐(PARTITION BY 있음) 통째로 보느냐(없음)에 따라 의미가 완전히 달라지죠.


🎯 [과제 3 예시답안] 증감과 인라인 뷰

채점 포인트

항목 배점 핵심
(가) LAG 증감 + 음수만 거르기(인라인 뷰) 55% 줄어든 행 3건(김유나 -25·김재훈 -10·강지수 -5)
(나) FIRST_VALUE 로 첫 달 붙이고 증가폭 비교 45% 1월 대비 6월 최대 증가 = 강지수 +90

풀이 예시

(가) 전월 대비 줄어든 달만 골라내기

LAG 로 전월 값을 끌어와 증감(diff)을 구한 뒤, 음수인 행만 거르고 싶어요. 그런데 diff 는 윈도우 함수라 WHERE 에 직접 못 쓰죠. 그래서 증감을 먼저 만든 쿼리를 인라인 뷰로 감싸고 바깥에서 걸러요.

SQL
SELECT nickname, mon, follower_count, diff
FROM (
  SELECT m.nickname, TO_CHAR(stat_month, 'YYYY-MM') AS mon, follower_count,
         follower_count
           - LAG(follower_count) OVER (PARTITION BY s.member_id ORDER BY stat_month) AS diff
  FROM   account_monthly_stat s
  JOIN   member m ON m.member_id = s.member_id
)
WHERE diff < 0
ORDER BY diff;
nickname mon follower_count diff
김유나 2026-03 185 -25
김재훈 2026-04 110 -10
강지수 2026-05 255 -5

팔로워가 줄어든 달은 딱 3건이에요. 각 회원의 1월 행은 diff 가 NULL(전월이 없음)이라 diff < 0 조건에서 자연히 빠져요.

(나) 1월 대비 6월 증가폭이 가장 큰 회원

FIRST_VALUE 로 각 회원의 첫 달(1월) 값을, 정렬을 뒤집은 FIRST_VALUE 로 마지막 달(6월) 값을 모든 행에 붙인 뒤, 회원당 한 줄로 추려 증가폭을 비교해요.

SQL
SELECT nickname, jan, jun, jun - jan AS growth
FROM (
  SELECT DISTINCT m.nickname,
    FIRST_VALUE(follower_count) OVER (PARTITION BY s.member_id ORDER BY stat_month)      AS jan,
    FIRST_VALUE(follower_count) OVER (PARTITION BY s.member_id ORDER BY stat_month DESC) AS jun
  FROM   account_monthly_stat s
  JOIN   member m ON m.member_id = s.member_id
)
ORDER BY growth DESC;
nickname jan jun growth
강지수 200 290 +90
박지훈 90 175 +85
김유나 180 250 +70
김재훈 100 145 +45

1월 대비 6월에 가장 많이 늘어난 회원은 강지수(+90)예요.

한 걸음 더

⚠️ 여기서 흔한 실수 하나. "6월만 보면 되니까" 하고 WHERE stat_month = DATE '2026-06-01' 을 먼저 붙이면 결과가 전부 증가폭 0 으로 나와요. WHERESELECT(윈도우 계산)보다 먼저 돌아서, 창에 6월 한 행만 남거든요. 그러면 FIRST_VALUE 가 보는 "첫 달" 도 "마지막 달" 도 똑같이 6월이 돼 버리죠. 그래서 창은 6개월 전체로 두고 계산한 뒤, DISTINCT 로 회원당 한 줄로 줄여야 해요. 윈도우 함수에서 "거르는 시점" 이 결과를 어떻게 바꾸는지 보여주는 좋은 예예요.


생각해볼 주제 예시답안

🤔 [생각해볼 주제 1] 동점을 인정할 것인가, 한 줄로 자를 것인가

같은 값을 받은 행이 여럿일 때 어떤 순위 함수를 쓸지는 "동점을 인정하느냐" 한 가지로 갈려요.

좋아요 랭킹 화면처럼 사람이 보는 등수표는 동점을 인정하는 게 자연스러워요. "공동 4위" 가 둘인데 한 명만 4위, 다른 한 명을 5위로 적으면 사용자가 불공정하다고 느끼니까요. 이럴 땐 RANK(공동 순위 + 다음 등수 건너뜀)나 DENSE_RANK(공동 순위 + 안 건너뜀)를 써요. 등수표를 빈칸 없이 촘촘히 보여주고 싶으면 DENSE_RANK, 진짜 등수처럼 동점 수만큼 건너뛰는 게 맞으면 RANK 고요.

반대로 동점을 절대 허용하면 안 되는 경우도 있어요. "선착순 한 명 당첨", "페이지네이션으로 한 페이지에 정확히 10개씩" 같은 경우엔 동점이어도 단 하나의 순서를 정해야 해요. 이때는 ROW_NUMBER 를 쓰고, 동점일 때 누가 앞에 올지를 ORDER BY 에 보조 기준(예: 먼저 만들어진 순서, id 순서)을 더해 확실히 고정해요. 그래야 같은 쿼리를 두 번 돌려도 결과 순서가 흔들리지 않거든요.

🎯 면접관을 홀리는 핵심 멘트

"순위 함수 선택은 '동점을 인정하느냐'로 갈립니다. 공동 순위를 보여줄 랭킹이면 RANK·DENSE_RANK, 동점이어도 반드시 하나로 잘라야 하는 선착순·페이지네이션이면 ROW_NUMBER를 쓰고, 이때는 ORDER BY에 보조 키를 더해 순서를 결정적으로 고정합니다."


🤔 [생각해볼 주제 2] 집계로 풀 수 있는 걸 왜 윈도우로 풀까

회원별 게시물 수 같은 값은 C-4 의 GROUP BY 로도 구할 수 있어요. 문제는 "게시물 목록은 전부 보여주면서, 각 줄 옆에 그 회원의 게시물 수도 같이" 가 필요할 때예요.

GROUP BY 만으로는 안 돼요. 집계는 행을 뭉쳐 버려서 원본 게시물 줄이 사라지거든요. 그래서 전통적으로는 GROUP BY 로 회원별 집계를 따로 구한 뒤, 그 결과를 다시 원본 게시물 표와 조인해 붙여야 했어요. 쿼리가 두 단(집계 + 조인)으로 길어지고, 같은 표를 두 번 읽는 셈이죠.

윈도우 함수는 이걸 한 번에 풀어요. COUNT(*) OVER (PARTITION BY member_id) 처럼 쓰면 게시물 줄을 그대로 둔 채 각 줄 옆에 그 회원의 게시물 수가 붙어요. 표를 한 번만 읽고, 쿼리도 짧아지고, 읽기도 좋죠. 다만 "원본 행은 필요 없고 회원별 요약만 보면 된다" 면 굳이 윈도우를 쓸 이유가 없어요. 그땐 GROUP BY 가 더 간단하고 결과도 가벼워요. 원본 행을 살려야 하면 윈도우, 뭉쳐도 되면 집계, 이렇게 갈라요.

🎯 면접관을 홀리는 핵심 멘트

"원본 행을 유지한 채 요약값을 옆에 붙여야 하면 윈도우 함수가 답입니다. GROUP BY로 집계해 다시 원본과 조인하면 표를 두 번 읽고 쿼리도 길어지지만, COUNT(*) OVER (PARTITION BY ...)는 한 번의 스캔으로 행을 보존하며 붙입니다. 반대로 요약만 필요하면 GROUP BY가 더 가볍습니다."


🤔 [생각해볼 주제 3] 순위 계산을 DB 에서 할까, 애플리케이션에서 할까

좋아요 랭킹을 데이터베이스가 RANK 로 매겨 줄 수도 있고, 게시물 목록만 받아 와 애플리케이션 코드가 정렬하며 매길 수도 있어요. 어느 쪽이 나은지는 데이터 양과 갱신 빈도에 달렸어요.

데이터베이스에서 순위를 매기면, 정렬과 순위 계산을 데이터에 가장 가까운 곳에서 하고 필요한 상위 몇 개만 네트워크로 넘겨받을 수 있어요. 수십만 건 중 상위 10개만 보여주는 화면이라면, 전부 애플리케이션으로 끌어와 정렬하는 것보다 데이터베이스가 추려 주는 쪽이 네트워크로 오가는 데이터도, 애플리케이션 메모리도 훨씬 아껴요. 인덱스가 받쳐 주면 정렬 비용도 줄고요.

다만 순위를 실시간으로 아주 자주 갱신해야 하거나(예: 초 단위로 바뀌는 실시간 랭킹), 데이터베이스 부하를 한곳에 몰면 안 되는 상황이라면 이야기가 달라져요. 이런 경우엔 순위를 미리 계산해 캐시에 올려 두거나, 별도의 순위 전용 저장소를 쓰는 선택도 해요. 기본은 "데이터가 많고 일부만 필요하면 데이터베이스에서", "갱신이 너무 잦고 부하가 문제면 캐시·전용 저장소로" 가 출발점이에요.

🎯 면접관을 홀리는 핵심 멘트

"대용량에서 상위 N개만 필요하면 순위 계산은 데이터베이스에 맡기는 게 정석입니다. 데이터에 가장 가까운 곳에서 정렬·추출해 네트워크와 애플리케이션 메모리를 아끼니까요. 단, 실시간 갱신이 잦거나 DB 부하가 병목이면 미리 계산해 캐시에 얹는 전략으로 분리합니다."

전체 목록 데이터베이스