[SQL/세션] 3회차 - WINDOW 함수 | 문제풀이

세션2회차 문제2번

프로그래머스 문제 링크 https://school.programmers.co.kr/learn/courses/30/lessons/164668

 

> 내 풀이 :  join 과 having 으로 필터링햔 풀이

SELECT u.user_id
        , u.nickname
        , SUM(price) AS TOTAL_PRICE
FROM used_goods_board AS b
LEFT JOIN used_goods_user AS u
ON b.writer_id=u.user_id
WHERE b.status = 'DONE'
GROUP BY u.user_id, u.nickname
HAVING SUM(price) >= '700000'
ORDER BY TOTAL_PRICE ASC
;

 

> from절 서브쿼리 풀이

SELECT 
    U.USER_ID,
    U.NICKNAME,
    S.TOTAL_SALES
FROM USED_GOODS_USER U
JOIN (
    SELECT WRITER_ID, SUM(PRICE) AS TOTAL_SALES
    FROM USED_GOODS_BOARD
    WHERE STATUS = 'DONE'
    GROUP BY WRITER_ID
    HAVING SUM(PRICE) >= 700000
) S
ON U.USER_ID = S.WRITER_ID
ORDER BY S.TOTAL_SALES ASC;

 

> CTE 절 사용 : 재사용 가능

WITH SALES AS (
    SELECT WRITER_ID, SUM(PRICE) AS TOTAL_SALES
    FROM USED_GOODS_BOARD
    WHERE STATUS = 'DONE'
    GROUP BY WRITER_ID
)
SELECT 
    U.USER_ID,
    U.NICKNAME,
    S.TOTAL_SALES
FROM SALES S
JOIN USED_GOODS_USER U
  ON U.USER_ID = S.WRITER_ID
WHERE S.TOTAL_SALES >= 700000
ORDER BY S.TOTAL_SALES ASC;

💡 여기서는 WHERE 절에서 필터링을 한 이유?!

집계는 이미 CTE 안에서 끝났기 때문에 WHERE로 필터링 가능 !!

 

⭐️ CTE 쿼리 구조의 장점 : 단계적으로 나눌 수 있음 → 데이터 흐름을 직관적으로 이해 가능!

  • 집계단계 : 회원별 거래금액 합계 계산
  • 출력단계 : 회원 정보와 조인 후 필터링 및 정렬

2회차 문제3

프로그래머스 문제 링크 https://school.programmers.co.kr/learn/courses/30/lessons/59045

 

> 내 풀이 : JOIN을 사용한 풀이

  • 🚨 주의 : where절에 연산자(and, or)는 잘 묶어줘야 함! "그리고" 인지 "또는" 인지 구분!
SELECT i.animal_id
        , i.animal_type
        , i.name
FROM animal_ins AS i
INNER JOIN animal_outs AS o
ON i.animal_id=o.animal_id
WHERE i.sex_upon_intake LIKE 'Intact%' 
  AND (o.sex_upon_outcome LIKE 'Spayed%'
   OR o.sex_upon_outcome LIKE 'Neutered%')
ORDER BY o.animal_id ASC 
;

 

> CASE WHEN 을 사용한 풀이

SELECT i.ANIMAL_ID,
       i.ANIMAL_TYPE,
       i.NAME
FROM ANIMAL_INS i
JOIN ANIMAL_OUTS o
  ON i.ANIMAL_ID = o.ANIMAL_ID
WHERE CASE 
        WHEN i.SEX_UPON_INTAKE LIKE '%Intact%' THEN 'N'
        ELSE 'Y'
      END = 'N'
  AND CASE 
        WHEN o.SEX_UPON_OUTCOME LIKE '%Intact%' THEN 'N'
        ELSE 'Y'
      END = 'Y'
ORDER BY i.ANIMAL_ID;

✅ WHERE절의 풀이가 특이하다! 짚고 넘어가보자~

 

입소 시 상태 확인

  • i.SEX_UPON_INTAKE가 "Intact..." → 'N' (Not neutered, 중성화 X)
  • 그 외(Spayed Female, Neutered Male 등) → 'Y' (중성화 O)
  • 조건: = 'N' → 입소할 때 중성화되지 않은 동물만 남김.

출소 시 상태 확인

  • o.SEX_UPON_OUTCOME가 "Intact..." → 'N' (중성화 X)
  • 그 외(Spayed..., Neutered...) → 'Y' (중성화 O)
  • 조건: = 'Y' → 출소할 때 중성화된 동물만 남김.

따라서, 입소 당시엔 중성화 X(N), 출소 당시엔 중성화 O(Y)인 동물만 필터링!


윈도우 함수

모든 컬럼을 잃고 싶지 않을 때 사용

행과 행의 관계를 알기 쉽게 해줌

하나의 컬럼이 생성되기 때문에 SELECT 절에서 사용!!

 

⭐️윈도우 함수의 종류

분류 대표 함수 설명 GROUP BY 병행 여부
집계 함수 SUM(), MAX(), MIN(), AVG(), COUNT() 윈도우 범위 내 합계, 최대/최소, 평균, 개수 계산 가능
순위 함수 RANK(), DENSE_RANK(), ROW_NUMBER() 행에 순위를 매기거나 행 번호 부여 불가능
순서 함수 FIRST_VALUE(), LAST_VALUE(), LAG(), LEAD() 파티션 내 첫 값/마지막 값, 이전/다음 행 값 참조 불가능
비율/
분석 함수
RATIO_TO_REPORT(), PERCENT_RANK(), CUME_DIST(), NTILE(n) 비율, 누적 백분율, 분위수 계산 불가능
-- 윈도우 함수 기본 문법

SELECT 
    WINDOW_FUNCTION(컬럼명) OVER ( -- OVER는 윈도우 함수가 계산될 범위를 정하는 키워드
        PARTITION BY 컬럼명   -- 파티션 기준 그룹화
        ORDER BY 컬럼명       -- 파티션 내 정렬 기준
    ) AS 별칭
FROM 
    테이블명;
    
    
SELECT WINDOW_FUNCTION(컬럼명) OVER (PARTITION BY 컬럼명 ORDER BY 컬럼명) AS 별칭
FROM 테이블명;

예시) ROW_NUMBER() OVER(PARTITION BY 컬럼1 ORDER BY 컬럼2)
-> ROW_NUMBER 라는 윈도우 함수를 사용하고, 그 기준을 컬럼1로 , 정렬은 컬럼2로 지정
구성요소 설명
OVER 윈도우 함수 범위를 지정하는 핵심 키워드
PARTITION BY 그룹핑 기준 (마치 GROUP BY처럼 작동하지만, 행은 그대로 유지)
ORDER BY 정렬 기준 (순위, 누적 계산 등에 필요)

 

▼ 윈도우 함수 괄호 안에 컬럼 필요 여부 정리

더보기
함수 이름 괄호 안에 컬럼 필요 여부 설명 쿼리 예시
RANK() 없음 순위 (중복 순위 있음, 건너뜀) RANK() OVER (ORDER BY salary DESC) AS rank_no
DENSE_RANK() 없음 순위 (중복 순위 있음, 건너뜀 없음) DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_no
ROW_NUMBER() 없음 고유 순번 부여 ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_no
PERCENT_RANK() 없음 백분율 순위 (0~1 사이) PERCENT_RANK() OVER (ORDER BY salary DESC) AS pct_rank
CUME_DIST() 없음 누적 백분율 (현재 이하 비율) CUME_DIST() OVER (ORDER BY salary DESC) AS cume_dist
NTILE(N) 있음 (N 숫자) N등분으로 나눠 등급 부여 NTILE(4) OVER (ORDER BY salary DESC) AS quartile
LAG(컬럼) 있음 이전 행의 LAG(salary) OVER (ORDER BY salary) AS prev_salary
LAG(컬럼, 숫자) 있음 N번째 이전 행의 LAG(salary, 2) OVER (ORDER BY salary) AS prev2_salary
LEAD(컬럼) 있음 이후 행의 LEAD(salary) OVER (ORDER BY salary) AS next_salary
LEAD(컬럼, 숫자) 있음 N번째 이후 행의 LEAD(salary, 3) OVER (ORDER BY salary) AS next3_salary
FIRST_VALUE(컬럼) 있음 파티션 번째 FIRST_VALUE(salary) OVER (PARTITION BY department_id ORDER BY salary DESC) AS top_salary
LAST_VALUE(컬럼) 있음 파티션 마지막 LAST_VALUE(salary) OVER (PARTITION BY department_id ORDER BY salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS bottom_salary
RATIO_TO_REPORT(컬럼) 있음 전체합 대비 현재값 비율 (Oracle) RATIO_TO_REPORT(salary) OVER () AS salary_ratio
SUM(컬럼) 있음 누적 합계 (윈도우 함수일 경우) SUM(salary) OVER (PARTITION BY department_id ORDER BY salary) AS running_total
AVG(컬럼) 있음 누적 평균 AVG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS running_avg

 

⭐️ 은 가장 많이 쓰이는 함수!!

함수 정의 특징
RANK() ORDER BY 기준 순위 부여 동순위같은 , 다음 순위 건너뜀 (: 1,2,2,4)
DENSE_RANK() RANK 동일하나 순위 건너뛰지 않음 동순위같은 , 다음 순위 연속 (: 1,2,2,3)
ROW_NUMBER() ⭐️ 행에 고유 번호 부여 중복값 있어도 고유 번호 (: 1,2,3,4)
FIRST_VALUE() 파티션(그룹) 내에서 정렬 기준상 번째 값을 반환 파티션별 정렬 번째 가져옴
LAST_VALUE() 파티션(그룹) 내에서 정렬 기준상 마지막 값을 반환 기본은 현재 행까지의 마지막 옵션 필요
LAG() ⭐️ 이전 가져옴 기본 N=1, 없으면 NULL
LEAD() ⭐️ 다음 가져옴 기본 N=1, 없으면 NULL
PERCENT_RANK() 백분위 순위(0~1) (순위-1) / (전체행수-1)
CUME_DIST() 누적 비율(0~1) 현재 이하 비율항상 마지막 값은 1
NTILE(N) 데이터를 N등분해 구간 번호 부여 1~N 등분, 분위수 계산에 활용
RATIO_TO_REPORT() 전체합 대비 비율 Oracle 전용, 합계 1.0 (100%)

 

📌 윈도우 함수를 쓰는 대표적인 상황 (드래그 해서 확인)

1. 순위 매기기(ranking) → 예: 부서별 급여 순위를 매겨라 (부서별로 높은 급여 순위를 계산하면서, 모든 직원 행을 그대로 유지)

   ✅ RANK( ), DENSE_RANK( ), ROW_NUMBER( )

 

2. 누적값, 이동평균 → 예: 월별 매출 누적 합계를 구하라 (각 달 매출 행은 그대로 두고, 누적 합계 컬럼 추가)

   ✅ SUM( ) OVER, AVG( ) OVER

 

3. 비교 (이전/다음 행 값 참조) → 직원의 급여가 직전 직원보다 얼마나 증가했는가? (같은 테이블 내 행 간 비교 가능)

   ✅ LAG( ), LEAD( )

 

4. 비율 계산 → 부서별 급여에서 각 직원 급여가 차지하는 비율 (각 행의 급여가 부서 전체 합계에서 몇 %인지 계산)

   ✅ RATIO_TO_REPORT( ), PERCENT_RANK( ), CUME_DIST( )

 

5. 특정 위치 값 가져오기 → 부서별 최고 급여자, 최저 급여자(파티션(그룹) 내에서 정렬 기준으로 맨 앞/맨 뒤 행의 값을 가져옴)

   ✅ FIRST_VALUE( ), LAST_VALUE( )

 

 

정리

💡 윈도우 함수는 데이터를 줄이지 않고 행 그대로 유지하면서 추가적인 통계·순위·누적값·비교값을 컬럼으로 붙이고 싶을 때 씁니다. 집계 함수(SUM, AVG)는 데이터를 줄이고 요약 윈도우 함수는 데이터를 그대로 두고 확장

3회차 문제1 (코드카타48번)

https://school.programmers.co.kr/learn/courses/30/lessons/131123

🚨 어려워했던 문제!!

 

> 질문해서 푼 풀이

# 튜플 사용
SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM REST_INFO
WHERE (FOOD_TYPE, FAVORITES) IN (
    SELECT FOOD_TYPE, MAX(FAVORITES)
    FROM REST_INFO
    GROUP BY FOOD_TYPE
)
ORDER BY FOOD_TYPE DESC;

 

💡 음식종류별로 즐겨찾기수가 가장 많은 식당 ?

  • 그룹 바이?
  • where절 서브쿼리?
  • 가장 많은? max() 함수?

 

> 다른풀이1 : where 절 서브쿼리(중첩(일반) 서브쿼리)

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM REST_INFO R
WHERE FAVORITES = (
    SELECT MAX(FAVORITES)
    FROM REST_INFO
    WHERE FOOD_TYPE = R.FOOD_TYPE
)
ORDER BY FOOD_TYPE DESC;

💡 서브쿼리 : 음식 종류별로 즐겨찾기 수가 가장 많은 것들 조건으로 필터링 한다!!  동적 조건(조건의 유동성)

최대인데, 최대가 뭔지를 모르겠어... 음식 종류별로 유동적으로 최대가 바뀌겠네?

➡️ 그럴땐 where절 서브쿼리!!!

 

> 다른풀이2 : 서브쿼리로 음식 종류별 즐겨찾기 최대값을 구한 뒤, 원본 테이블과 조인해서 해당 식당의 정보를 가져오는 방식

SELECT R.FOOD_TYPE, R.REST_ID, R.REST_NAME, R.FAVORITES
FROM REST_INFO R
JOIN (
    SELECT FOOD_TYPE, MAX(FAVORITES) AS MAX_FAV
    FROM REST_INFO
    GROUP BY FOOD_TYPE
) M
ON R.FOOD_TYPE = M.FOOD_TYPE
AND R.FAVORITES = M.MAX_FAV
ORDER BY R.FOOD_TYPE DESC;

💡 내부 서브쿼리 (M) → 외부 쿼리와 JOIN  : 음식종류가 같은 것으로 즐찾수 많은 

 

> 다른풀이3 : CTE + JOIN 방식

WITH MAX_FAV AS (
    SELECT FOOD_TYPE, MAX(FAVORITES) AS MAX_FAV
    FROM REST_INFO
    GROUP BY FOOD_TYPE
)
SELECT R.FOOD_TYPE, R.REST_ID, R.REST_NAME, R.FAVORITES
FROM REST_INFO R
JOIN MAX_FAV M
  ON R.FOOD_TYPE = M.FOOD_TYPE
 AND R.FAVORITES = M.MAX_FAV
ORDER BY R.FOOD_TYPE DESC;

💡 2번째 풀이와 비슷하다.

 

> 다른풀이4 : 윈도우 함수 사용

최대? 음식종류 별로 즐찾수가 가장 많아? 오 윈도우 함수 가능할지도!!? 그룹내에서 순위를 매기고 1등만 뽑기 때문!

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM (
    SELECT 
        FOOD_TYPE,
        REST_ID,
        REST_NAME,
        FAVORITES,
        ROW_NUMBER() OVER (PARTITION BY FOOD_TYPE ORDER BY FAVORITES DESC) AS RN
    FROM REST_INFO
) A
WHERE RN = 1
ORDER BY FOOD_TYPE DESC;

▼ 왜 rank()나 dense_rank()가 아니고 row_number()?

더보기
  • RANK() → 동점이 있으면 같은 순위(1, 1, 3…)
  • DENSE_RANK() → 동점이 있으면 같은 순위지만 다음 순위는 바로 이어짐(1, 1, 2…)
  • ROW_NUMBER() → 무조건 1개만 뽑힘 (동점도 강제로 순서가 매겨짐 → 1, 2, 3…)

 

▼ 최종 정리 (wher절 서브쿼리 / join 방식 / 윈도우 함수 방식)

더보기
구분 WHERE 서브쿼리 방식 JOIN 방식 윈도우 함수 방식
동작 원리 바깥 테이블의 행마다 서브쿼리를 실행해 해당 FOOD_TYPE MAX(FAVORITES) 비교 서브쿼리에서 음식 종류별 최댓값 테이블(M) 만들고, 원본 테이블과 매칭 ROW_NUMBER() OVER (PARTITION BY FOOD_TYPE ORDER BY FAVORITES DESC) 그룹별 순위를 매기고, 1등만 선택
가독성 간단하고 직관적 (짧은 코드) 최댓값 테이블조인 구조가 명확 그룹별 순위 매기고 1 뽑는다 개념이 직관적
성능 바깥 행마다 서브쿼리 실행대용량에서 비효율적 그룹별 최댓값을 번만 계산 조인효율적 순위 계산을 번만 하고 바로 필터링대규모 데이터에도 효율적
확장성 단순 최대값 비교까지만 적합 평균, 최소값 다른 집계값을 함께 활용하기 쉽다 다양한 순위 로직(RANK, DENSE_RANK) 적용 가능동점 처리 유연
동점 처리 최대값과 같은 여러 모두 출력됨 최대값과 같은 여러 모두 출력됨 ROW_NUMBER() 동점 1개만, RANK/DENSE_RANK 동점 모두 출력 가능
권장 상황 데이터가 적거나 학습용으로 간단히 실무에서 집계값을 다른 조건과 함께 활용할 실무에서 직관적이고 확장성·성능까지 모두 고려할

3회차 문제2 (코드카타49번)

https://school.programmers.co.kr/learn/courses/30/lessons/131116

 

> 내 풀이

SELECT category
        , MAX(price) AS max_price
        , product_name
FROM food_product
WHERE (category, price) IN (SELECT category, max(price) as max_price
                           FROM food_product
                           WHERE category in ('과자', '국', '김치', '식용유')
                            GROUP BY category
                           )
GROUP BY category
ORDER BY price DESC;
  • 분류별(CATEGORY)로 묶어야 함.
  • 각 분류에서 가장 비싼 가격(최댓값)을 찾아야 함
  • 출력은 분류, 최고가, 상품명
  • 단, 카테고리는 '과자', '국', '김치', '식용유'만
  • 최종적으로 가격 내림차순 정렬
  • <GROUP BY + MAX> 문제처럼 보이지만, 상품명까지 출력해야 하기 때문에 단순 집계만으로는 안 되겠네!

> 다른풀이1 : 윈도우 함수

SELECT CATEGORY,
       PRICE AS MAX_PRICE,
       PRODUCT_NAME
FROM (
    SELECT CATEGORY,
           PRODUCT_NAME,
           PRICE,
           ROW_NUMBER() OVER (
               PARTITION BY CATEGORY 
               ORDER BY PRICE DESC
           ) AS RN
    FROM FOOD_PRODUCT
    WHERE CATEGORY IN ('과자', '국', '김치', '식용유')
) T
WHERE RN = 1
ORDER BY MAX_PRICE DESC;

 

> 다른풀이2 : 집계 + JOIN 방식

SELECT FP.CATEGORY,
       FP.PRICE AS MAX_PRICE,
       FP.PRODUCT_NAME
FROM FOOD_PRODUCT FP
JOIN (
    SELECT CATEGORY, MAX(PRICE) AS MAX_PRICE
    FROM FOOD_PRODUCT
    WHERE CATEGORY IN ('과자', '국', '김치', '식용유')
    GROUP BY CATEGORY
) TMP
  ON FP.CATEGORY = TMP.CATEGORY
 AND FP.PRICE = TMP.MAX_PRICE
WHERE FP.CATEGORY IN ('과자', '국', '김치', '식용유')
ORDER BY FP.PRICE DESC;
🎯 정리해보자면,
집계 + JOIN 방식이 결합된 풀이가 많이 나온다.
그리고 윈도우 함수도 종종 나오고 나오면 헷깔릴 수 있으니 복습 철저히 하자! 😎

끝.