SQL 면접 대비
사용 테이블
orders — 주문 (상태, 금액, 일시)
order_items — 주문 상세 (수량, 단가)
products — 상품 (이름, 가격, 재고, 브랜드)
categories — 카테고리 (부모-자식 계층)
reviews — 리뷰 (평점, 내용)
customers — 고객 (등급, 포인트, 가입채널)
학습 범위
ROW_NUMBER, LAG, Running Total, Moving Average, Recursive CTE, NTILE, Funnel
1. Top-N per Group
각 카테고리에서 매출 1위 상품을 구하세요. (동률 시 하나만)
힌트 1: ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC)로
그룹 내 순위를 매기고, WHERE rn = 1로 필터링합니다.
정답
WITH ranked AS (
SELECT
cat.name AS category,
p.name AS product,
ROUND(SUM(oi.quantity * oi.unit_price), 0) AS revenue,
ROW_NUMBER() OVER (PARTITION BY cat.id ORDER BY SUM(oi.quantity * oi.unit_price) DESC) AS rn
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
INNER JOIN products p ON oi.product_id = p.id
INNER JOIN categories cat ON p.category_id = cat.id
WHERE o.status NOT IN ('cancelled')
GROUP BY cat.id, cat.name, p.id, p.name
)
SELECT category, product, revenue
FROM ranked
WHERE rn = 1
ORDER BY revenue DESC;
실행 결과 (총 40행 중 상위 7행)
| category | product | revenue |
|---|---|---|
| 게이밍 노트북 | Razer Blade 18 블랙 | 1,179,690,100.00 |
| NVIDIA | ASUS Dual RTX 4060 Ti 블랙 | 901,407,600.00 |
| AMD | MSI Radeon RX 7900 XTX GAMING X 화이트 | 602,487,200.00 |
| 2in1 | 레노버 ThinkPad X1 2in1 실버 | 582,223,200.00 |
| Intel | Intel Core Ultra 5 245KF | 563,595,600.00 |
| 일반 노트북 | ASUS ExpertBook B5 [특별 한정판 에디션] 저소음 설... | 555,152,000.00 |
| 게이밍 모니터 | 삼성 오디세이 G5 27 블랙 | 538,902,000.00 |
2. 연속 증가 구간
월별 매출이 3개월 연속 증가한 구간을 찾으세요.
힌트 1: LAG(revenue, 1)과 LAG(revenue, 2)로 이전 2개월 매출을 가져와서
현재 > 전월 > 전전월 조건 비교.
정답
WITH monthly AS (
SELECT
SUBSTR(ordered_at, 1, 7) AS month,
SUM(total_amount) AS revenue
FROM orders
WHERE status NOT IN ('cancelled')
GROUP BY SUBSTR(ordered_at, 1, 7)
),
with_lag AS (
SELECT
month, revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_1,
LAG(revenue, 2) OVER (ORDER BY month) AS prev_2
FROM monthly
)
SELECT month, revenue, prev_1, prev_2
FROM with_lag
WHERE revenue > prev_1 AND prev_1 > prev_2
ORDER BY month;
실행 결과 (총 22행 중 상위 7행)
| month | revenue | prev_1 | prev_2 |
|---|---|---|---|
| 2016-04 | 16,878,372.00 | 14,806,662.00 | 13,115,835.00 |
| 2016-05 | 31,432,968.00 | 16,878,372.00 | 14,806,662.00 |
| 2016-10 | 38,989,248.00 | 32,257,785.00 | 19,122,587.00 |
| 2018-01 | 85,442,344.00 | 83,561,937.00 | 52,363,290.00 |
| 2018-09 | 134,594,321.00 | 85,713,577.00 | 40,611,909.00 |
| 2018-12 | 167,642,638.00 | 93,642,089.00 | 63,988,837.00 |
| 2019-01 | 228,770,158.00 | 167,642,638.00 | 93,642,089.00 |
3. 누적 합계 (Running Total)
2024년 월별 매출과 누적 매출을 구하세요.
힌트 1: SUM(monthly_revenue) OVER (ORDER BY month)로 누적 합계를 구합니다.
정답
SELECT
SUBSTR(ordered_at, 1, 7) AS month,
ROUND(SUM(total_amount), 0) AS monthly_revenue,
ROUND(SUM(SUM(total_amount)) OVER (ORDER BY SUBSTR(ordered_at, 1, 7)), 0) AS cumulative_revenue
FROM orders
WHERE ordered_at LIKE '2024%'
AND status NOT IN ('cancelled')
GROUP BY SUBSTR(ordered_at, 1, 7)
ORDER BY month;
실행 결과 (총 12행 중 상위 7행)
| month | monthly_revenue | cumulative_revenue |
|---|---|---|
| 2024-01 | 301,075,320.00 | 301,075,320.00 |
| 2024-02 | 426,177,449.00 | 727,252,769.00 |
| 2024-03 | 536,322,767.00 | 1,263,575,536.00 |
| 2024-04 | 470,154,081.00 | 1,733,729,617.00 |
| 2024-05 | 459,724,596.00 | 2,193,454,213.00 |
| 2024-06 | 377,040,302.00 | 2,570,494,515.00 |
| 2024-07 | 363,944,597.00 | 2,934,439,112.00 |
4. 이동 평균 (Moving Average)
3개월 이동 평균 매출을 구하세요.
힌트 1: AVG(revenue) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
정답
WITH monthly AS (
SELECT
SUBSTR(ordered_at, 1, 7) AS month,
ROUND(SUM(total_amount), 0) AS revenue
FROM orders
WHERE status NOT IN ('cancelled')
GROUP BY SUBSTR(ordered_at, 1, 7)
)
SELECT
month,
revenue,
ROUND(AVG(revenue) OVER (
ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 0) AS moving_avg_3m
FROM monthly
ORDER BY month;
실행 결과 (총 120행 중 상위 7행)
| month | revenue | moving_avg_3m |
|---|---|---|
| 2016-01 | 15,816,169.00 | 15,816,169.00 |
| 2016-02 | 13,115,835.00 | 14,466,002.00 |
| 2016-03 | 14,806,662.00 | 14,579,555.00 |
| 2016-04 | 16,878,372.00 | 14,933,623.00 |
| 2016-05 | 31,432,968.00 | 21,039,334.00 |
| 2016-06 | 26,381,091.00 | 24,897,477.00 |
| 2016-07 | 30,436,884.00 | 29,416,981.00 |
5. 갭 분석 (Missing Data)
2024년에 주문이 없는 날짜를 찾으세요.
힌트 1: WITH RECURSIVE로 2024년 모든 날짜를 생성한 뒤,
실제 주문 날짜와 LEFT JOIN하여 NULL인 날을 찾습니다.
정답
WITH RECURSIVE all_dates AS (
SELECT '2024-01-01' AS dt
UNION ALL
SELECT DATE(dt, '+1 day')
FROM all_dates
WHERE dt < '2024-12-31'
),
order_dates AS (
SELECT DISTINCT SUBSTR(ordered_at, 1, 10) AS dt
FROM orders
WHERE ordered_at LIKE '2024%'
)
SELECT ad.dt AS missing_date
FROM all_dates ad
LEFT JOIN order_dates od ON ad.dt = od.dt
WHERE od.dt IS NULL
ORDER BY ad.dt;
6. 백분위수 (Percentile)
고객 구매 금액의 상위 10%, 25%, 50%(중앙값), 75%, 90% 지점을 구하세요.
힌트 1: NTILE(100) OVER (ORDER BY total_spent)로 백분위 그룹을 만들고,
MAX(CASE WHEN percentile = N ...)로 각 지점의 값을 추출합니다.
정답
WITH customer_spend AS (
SELECT
customer_id,
SUM(total_amount) AS total_spent
FROM orders
WHERE status NOT IN ('cancelled')
GROUP BY customer_id
),
ranked AS (
SELECT
total_spent,
NTILE(100) OVER (ORDER BY total_spent) AS percentile
FROM customer_spend
)
SELECT
MAX(CASE WHEN percentile = 10 THEN total_spent END) AS p10,
MAX(CASE WHEN percentile = 25 THEN total_spent END) AS p25,
MAX(CASE WHEN percentile = 50 THEN total_spent END) AS p50_median,
MAX(CASE WHEN percentile = 75 THEN total_spent END) AS p75,
MAX(CASE WHEN percentile = 90 THEN total_spent END) AS p90
FROM ranked;
실행 결과 (1행)
| p10 | p25 | p50_median | p75 | p90 |
|---|---|---|---|---|
| 180,500.00 | 1,183,200.00 | 4,654,232.00 | 13,607,591.00 | 31,606,150.00 |
7. 연도별 순위 변동
카테고리별 연도별 매출 순위를 구하고, 전년 대비 순위 변동을 표시하세요.
힌트 1: RANK() OVER (PARTITION BY year ORDER BY revenue DESC)로 연도별 순위를 매기고,
LAG(rank) OVER (PARTITION BY category ORDER BY year)로 전년 순위를 가져옵니다.
정답
WITH yearly_category AS (
SELECT
SUBSTR(o.ordered_at, 1, 4) AS year,
cat.name AS category,
ROUND(SUM(oi.quantity * oi.unit_price), 0) AS revenue
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
INNER JOIN products p ON oi.product_id = p.id
INNER JOIN categories cat ON p.category_id = cat.id
WHERE o.status NOT IN ('cancelled')
GROUP BY SUBSTR(o.ordered_at, 1, 4), cat.name
),
with_rank AS (
SELECT *,
RANK() OVER (PARTITION BY year ORDER BY revenue DESC) AS rank
FROM yearly_category
)
SELECT
wr.year, wr.category, wr.revenue, wr.rank,
LAG(wr.rank) OVER (PARTITION BY wr.category ORDER BY wr.year) AS prev_rank,
LAG(wr.rank) OVER (PARTITION BY wr.category ORDER BY wr.year) - wr.rank AS rank_change
FROM with_rank wr
WHERE wr.year >= '2022'
ORDER BY wr.year, wr.rank;
실행 결과 (총 148행 중 상위 7행)
| year | category | revenue | rank | prev_rank | rank_change |
|---|---|---|---|---|---|
| 2022 | 게이밍 노트북 | 832,586,500.00 | 1 | NULL | NULL |
| 2022 | 게이밍 모니터 | 372,536,900.00 | 2 | NULL | NULL |
| 2022 | 일반 노트북 | 368,304,500.00 | 3 | NULL | NULL |
| 2022 | AMD | 360,526,000.00 | 4 | NULL | NULL |
| 2022 | NVIDIA | 331,064,700.00 | 5 | NULL | NULL |
| 2022 | AMD 소켓 | 257,637,700.00 | 6 | NULL | NULL |
| 2022 | Intel | 217,901,700.00 | 7 | NULL | NULL |
8. Funnel 분석 (퍼널)
고객 여정 퍼널을 분석하세요: 가입 -> 첫 주문 -> 리뷰 작성 -> 재구매. 각 단계의 전환율을 구하세요.
힌트 1: 각 단계의 고유 고객 수를 스칼라 서브쿼리로 구하고,
100.0 * next_step / prev_step로 전환율을 계산합니다.
정답
WITH funnel AS (
SELECT
(SELECT COUNT(*) FROM customers) AS step1_signup,
(SELECT COUNT(DISTINCT customer_id) FROM orders
WHERE status NOT IN ('cancelled')) AS step2_first_order,
(SELECT COUNT(DISTINCT customer_id) FROM reviews) AS step3_review,
(SELECT COUNT(*) FROM (
SELECT customer_id FROM orders
WHERE status NOT IN ('cancelled')
GROUP BY customer_id HAVING COUNT(*) >= 2
)) AS step4_repeat
)
SELECT
step1_signup,
step2_first_order,
ROUND(100.0 * step2_first_order / step1_signup, 1) AS cvr_1_2,
step3_review,
ROUND(100.0 * step3_review / step2_first_order, 1) AS cvr_2_3,
step4_repeat,
ROUND(100.0 * step4_repeat / step2_first_order, 1) AS cvr_2_4
FROM funnel;
실행 결과 (1행)
| step1_signup | step2_first_order | cvr_1_2 | step3_review | cvr_2_3 | step4_repeat | cvr_2_4 |
|---|---|---|---|---|---|---|
| 5230 | 2809 | 53.70 | 1899 | 67.60 | 2319 | 82.60 |
9. 자기 참조 계층 깊이 탐색
카테고리 계층의 최대 깊이와 각 깊이별 카테고리 수를 구하세요. 재귀 CTE를 사용하세요.
힌트 1: WITH RECURSIVE에서 parent_id IS NULL을 시작점(depth=0)으로,
c.parent_id = tree.id로 자식을 재귀 탐색합니다.
정답
WITH RECURSIVE tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, t.depth + 1
FROM categories c
INNER JOIN tree t ON c.parent_id = t.id
)
SELECT
depth,
COUNT(*) AS category_count,
GROUP_CONCAT(name, ', ') AS categories
FROM tree
GROUP BY depth
ORDER BY depth;
실행 결과 (2행)
| depth | category_count | categories |
|---|---|---|
| 0 | 18 | 데스크톱 PC, 노트북, 모니터, CPU, 메인보드, 메모리(RAM... |
| 1 | 35 | 베어본, 완제품, 조립PC, 2in1, 게이밍 노트북, 맥북, 일반... |
10. 동일 상품 재구매 간격
같은 상품을 2회 이상 구매한 고객의 평균 재구매 간격(일)을 구하세요.
힌트 1: LAG(ordered_at) OVER (PARTITION BY customer_id, product_id ORDER BY ordered_at)로
같은 고객-상품의 이전 주문일을 가져와 JULIANDAY 차이를 계산합니다.
정답
WITH repeat_purchases AS (
SELECT
o.customer_id,
oi.product_id,
o.ordered_at,
LAG(o.ordered_at) OVER (
PARTITION BY o.customer_id, oi.product_id
ORDER BY o.ordered_at
) AS prev_order_date
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
WHERE o.status NOT IN ('cancelled')
)
SELECT
ROUND(AVG(JULIANDAY(ordered_at) - JULIANDAY(prev_order_date)), 1) AS avg_repurchase_days,
MIN(CAST(JULIANDAY(ordered_at) - JULIANDAY(prev_order_date) AS INTEGER)) AS min_days,
MAX(CAST(JULIANDAY(ordered_at) - JULIANDAY(prev_order_date) AS INTEGER)) AS max_days,
COUNT(*) AS repurchase_count
FROM repeat_purchases
WHERE prev_order_date IS NOT NULL;
실행 결과 (1행)
| avg_repurchase_days | min_days | max_days | repurchase_count |
|---|---|---|---|
| 370.30 | 0 | 3240 | 22,778 |