중급 종합 문제
사용 테이블
orders — 주문 (상태, 금액, 일시)
customers — 고객 (등급, 포인트, 가입채널)
order_items — 주문 상세 (수량, 단가)
products — 상품 (이름, 가격, 재고, 브랜드)
categories — 카테고리 (부모-자식 계층)
payments — 결제 (방법, 금액, 상태)
reviews — 리뷰 (평점, 내용)
shipping — 배송 (택배사, 추적번호, 상태)
complaints — 고객 불만 (유형, 우선순위)
wishlists — 위시리스트 (고객-상품)
suppliers — 공급업체 (업체명, 연락처)
returns — 반품/교환 (사유, 상태)
coupon_usage — 쿠폰 사용 내역
학습 범위
JOIN, subquery, date functions, string functions, CASE, UNION, GROUP BY, HAVING, aggregate functions, LAG, window functions
1. JOIN + GROUP BY: 카테고리별 판매 중인 상품 수와 평균 가격을 조회하세요. 상품 수가 많은 순으
JOIN + GROUP BY: 카테고리별 판매 중인 상품 수와 평균 가격을 조회하세요. 상품 수가 많은 순으로 10개만.
힌트 1: products JOIN categories로 연결 후 GROUP BY로 카테고리별 집계. WHERE is_active = 1로 판매 중만 필터링.
정답
SELECT
cat.name AS category,
COUNT(*) AS product_count,
ROUND(AVG(p.price)) AS avg_price
FROM products p
INNER JOIN categories cat ON p.category_id = cat.id
WHERE p.is_active = 1
GROUP BY cat.id, cat.name
ORDER BY product_count DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| category | product_count | avg_price |
|---|---|---|
| 파워서플라이(PSU) | 11 | 234,645.00 |
| 게이밍 모니터 | 10 | 1,123,150.00 |
| Intel 소켓 | 10 | 527,080.00 |
| 케이스 | 10 | 159,930.00 |
| 조립PC | 9 | 1,836,467.00 |
| AMD 소켓 | 9 | 511,056.00 |
| 스피커/헤드셋 | 9 | 274,056.00 |
2. JOIN + CASE: 각 주문의 주문번호, 고객명, 결제 방법을 조회하세요. 결제 방법은 한글로 변환. 최
JOIN + CASE: 각 주문의 주문번호, 고객명, 결제 방법을 조회하세요. 결제 방법은 한글로 변환. 최근 10건.
힌트 1: orders JOIN customers JOIN payments로 3개 테이블 연결. CASE payments.method WHEN ... THEN ...으로 한글 변환.
정답
SELECT
o.order_number,
c.name AS customer_name,
CASE pay.method
WHEN 'card' THEN '카드'
WHEN 'bank_transfer' THEN '계좌이체'
WHEN 'kakao_pay' THEN '카카오페이'
WHEN 'naver_pay' THEN '네이버페이'
WHEN 'virtual_account' THEN '가상계좌'
WHEN 'point' THEN '포인트'
ELSE '기타'
END AS payment_method_kr,
o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN payments pay ON o.id = pay.order_id
ORDER BY o.ordered_at DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| order_number | customer_name | payment_method_kr | total_amount |
|---|---|---|---|
| ORD-20251231-37555 | 송지영 | 카드 | 74,800.00 |
| ORD-20251231-37543 | 박민서 | 카카오페이 | 134,100.00 |
| ORD-20251231-37552 | 강미경 | 카드 | 254,300.00 |
| ORD-20251231-37548 | 윤영희 | 계좌이체 | 187,700.00 |
| ORD-20251231-37542 | 문도현 | 카카오페이 | 155,700.00 |
| ORD-20251231-37546 | 박상호 | 네이버페이 | 198,300.00 |
| ORD-20251231-37547 | 강서준 | 계좌이체 | 335,000.00 |
3. 서브쿼리 + 날짜: 2024년에 가장 많이 주문한 고객 TOP 5의 이름, 주문 건수, 총 결제금액을 조회하
서브쿼리 + 날짜: 2024년에 가장 많이 주문한 고객 TOP 5의 이름, 주문 건수, 총 결제금액을 조회하세요.
힌트 1: orders에서 ordered_at LIKE '2024%'로 필터링 후 GROUP BY customer_id로 집계.
정답
SELECT
c.name,
sub.order_count,
sub.total_spent
FROM (
SELECT customer_id, COUNT(*) AS order_count, SUM(total_amount) AS total_spent
FROM orders
WHERE ordered_at LIKE '2024%'
GROUP BY customer_id
ORDER BY order_count DESC
LIMIT 5
) sub
INNER JOIN customers c ON sub.customer_id = c.id
ORDER BY sub.order_count DESC;
실행 결과 (5행)
| name | order_count | total_spent |
|---|---|---|
| 신은정 | 25 | 25,706,293.00 |
| 심승현 | 25 | 24,330,429.00 |
| 이수빈 | 24 | 28,684,563.00 |
| 이민지 | 23 | 24,524,212.00 |
| 김명숙 | 23 | 20,979,498.00 |
4. 문자열 함수 + GROUP BY: 고객 이메일 도메인별 고객 수를 집계하세요.
문자열 함수 + GROUP BY: 고객 이메일 도메인별 고객 수를 집계하세요.
힌트 1: SUBSTR(email, INSTR(email, '@') + 1)로 도메인을 추출합니다.
정답
SELECT
SUBSTR(email, INSTR(email, '@') + 1) AS domain,
COUNT(*) AS customer_count
FROM customers
GROUP BY domain
ORDER BY customer_count DESC;
실행 결과 (1행)
| domain | customer_count |
|---|---|
| testmail.kr | 5230 |
5. JOIN + HAVING: 리뷰를 5건 이상 작성한 고객의 이름과 평균 평점을 조회하세요. 상위 10명.
JOIN + HAVING: 리뷰를 5건 이상 작성한 고객의 이름과 평균 평점을 조회하세요. 상위 10명.
힌트 1: reviews JOIN customers로 연결 후 GROUP BY customer_id + HAVING COUNT(*) >= 5.
정답
SELECT
c.name,
COUNT(*) AS review_count,
ROUND(AVG(r.rating), 1) AS avg_rating
FROM reviews r
INNER JOIN customers c ON r.customer_id = c.id
GROUP BY r.customer_id, c.name
HAVING COUNT(*) >= 5
ORDER BY review_count DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| name | review_count | avg_rating |
|---|---|---|
| 김병철 | 72 | 3.80 |
| 박정수 | 63 | 3.80 |
| 정유진 | 63 | 4.10 |
| 이영자 | 62 | 4.00 |
| 강명자 | 62 | 3.80 |
| 이미정 | 60 | 3.60 |
| 김성민 | 59 | 4.20 |
6. UNION + CASE: 최근 1년간 신규 고객 수와 신규 상품 수를 하나의 결과로 합쳐서 보세요.
UNION + CASE: 최근 1년간 신규 고객 수와 신규 상품 수를 하나의 결과로 합쳐서 보세요.
힌트 1: 각 테이블에서 2024년 데이터를 COUNT하고 UNION ALL로 합칩니다.
정답
SELECT '신규 고객' AS category, COUNT(*) AS count_2024
FROM customers
WHERE created_at LIKE '2024%'
UNION ALL
SELECT '신규 상품' AS category, COUNT(*) AS count_2024
FROM products
WHERE created_at LIKE '2024%';
실행 결과 (2행)
| category | count_2024 |
|---|---|
| 신규 고객 | 700 |
| 신규 상품 | 30 |
7. 날짜 함수 + JOIN: 주문 후 배송 완료까지 걸린 평균 일수를 택배사별로 조회하세요.
날짜 함수 + JOIN: 주문 후 배송 완료까지 걸린 평균 일수를 택배사별로 조회하세요.
힌트 1: julianday(delivered_at) - julianday(ordered_at)로 일수 계산.
정답
SELECT
s.carrier,
COUNT(*) AS delivered_count,
ROUND(AVG(julianday(s.delivered_at) - julianday(o.ordered_at)), 1) AS avg_days
FROM shipping s
INNER JOIN orders o ON s.order_id = o.id
WHERE s.status = 'delivered' AND s.delivered_at IS NOT NULL
GROUP BY s.carrier
ORDER BY avg_days;
실행 결과 (4행)
| carrier | delivered_count | avg_days |
|---|---|---|
| CJ대한통운 | 13,671 | 4.50 |
| 로젠택배 | 6939 | 4.50 |
| 우체국택배 | 5207 | 4.50 |
| 한진택배 | 8701 | 4.50 |
8. 서브쿼리 + CASE: 각 상품의 카테고리 평균 가격 대비 고가/저가/평균 수준을 표시하세요. 상위 15개.
서브쿼리 + CASE: 각 상품의 카테고리 평균 가격 대비 고가/저가/평균 수준을 표시하세요. 상위 15개.
힌트 1: 서브쿼리로 카테고리별 평균 가격을 구한 뒤 JOIN. CASE WHEN price > avg_price * 1.2 THEN '고가'...
정답
SELECT
p.name, p.price, cat_avg.avg_price,
CASE
WHEN p.price > cat_avg.avg_price * 1.2 THEN '고가'
WHEN p.price < cat_avg.avg_price * 0.8 THEN '저가'
ELSE '평균 수준'
END AS price_level
FROM products p
INNER JOIN (
SELECT category_id, ROUND(AVG(price)) AS avg_price
FROM products WHERE is_active = 1
GROUP BY category_id
) cat_avg ON p.category_id = cat_avg.category_id
WHERE p.is_active = 1
ORDER BY p.price DESC
LIMIT 15;
실행 결과 (총 15행 중 상위 7행)
| name | price | avg_price | price_level |
|---|---|---|---|
| MacBook Air 15 M3 실버 | 5,481,100.00 | 5,481,100.00 | 평균 수준 |
| ASUS Dual RTX 5070 Ti [특별 한정판 에디션] 저소... | 4,496,700.00 | 2,207,600.00 | 고가 |
| Razer Blade 18 블랙 | 4,353,100.00 | 2,887,583.00 | 고가 |
| Razer Blade 16 실버 | 3,702,900.00 | 2,887,583.00 | 고가 |
| ASUS ROG Strix G16CH 화이트 | 3,671,500.00 | 1,836,467.00 | 고가 |
| ASUS ROG Strix GT35 | 3,296,800.00 | 1,836,467.00 | 고가 |
| Razer Blade 18 블랙 | 2,987,500.00 | 2,887,583.00 | 평균 수준 |
9. JOIN + GROUP BY + HAVING + 서브쿼리: 전체 평균보다 높은 매출을 올린 브랜드의 이름,
JOIN + GROUP BY + HAVING + 서브쿼리: 전체 평균보다 높은 매출을 올린 브랜드의 이름, 총 매출, 주문 건수를 조회하세요. 상위 10개.
힌트 1: HAVING SUM(subtotal) > (SELECT AVG(...)). 전체 평균은 브랜드별 매출의 평균입니다.
정답
SELECT
p.brand,
SUM(oi.subtotal) AS total_revenue,
COUNT(DISTINCT oi.order_id) AS order_count
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.id
GROUP BY p.brand
HAVING SUM(oi.subtotal) > (
SELECT AVG(brand_revenue)
FROM (
SELECT SUM(oi2.subtotal) AS brand_revenue
FROM order_items oi2
INNER JOIN products p2 ON oi2.product_id = p2.id
GROUP BY p2.brand
)
)
ORDER BY total_revenue DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| brand | total_revenue | order_count |
|---|---|---|
| ASUS | 6,181,415,600.00 | 4945 |
| Razer | 4,167,252,100.00 | 2754 |
| 삼성전자 | 3,000,828,600.00 | 7169 |
| MSI | 2,787,039,100.00 | 3531 |
| LG전자 | 2,221,778,900.00 | 1647 |
| ASRock | 1,741,420,800.00 | 3435 |
| Intel | 1,267,419,000.00 | 2579 |
10. 2024년 월별로 주문 상태 분포를 조회하세요. 완료, 취소, 기타 건수와 취소율을 표시합니다.
2024년 월별로 주문 상태 분포를 조회하세요. 완료, 취소, 기타 건수와 취소율을 표시합니다.
힌트 1: SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END)로 조건부 집계.
정답
SELECT
SUBSTR(ordered_at, 1, 7) AS month,
COUNT(*) AS total_orders,
SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) AS confirmed,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE WHEN status NOT IN ('confirmed', 'cancelled') THEN 1 ELSE 0 END) AS in_progress,
ROUND(100.0 * SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) / COUNT(*), 1) AS cancel_rate
FROM orders
WHERE ordered_at LIKE '2024%'
GROUP BY SUBSTR(ordered_at, 1, 7)
ORDER BY month;
실행 결과 (총 12행 중 상위 7행)
| month | total_orders | confirmed | cancelled | in_progress | cancel_rate |
|---|---|---|---|---|---|
| 2024-01 | 346 | 314 | 21 | 11 | 6.10 |
| 2024-02 | 465 | 416 | 32 | 17 | 6.90 |
| 2024-03 | 601 | 555 | 29 | 17 | 4.80 |
| 2024-04 | 506 | 466 | 28 | 12 | 5.50 |
| 2024-05 | 415 | 385 | 19 | 11 | 4.60 |
| 2024-06 | 415 | 389 | 18 | 8 | 4.30 |
| 2024-07 | 414 | 381 | 23 | 10 | 5.60 |
11. VIP 고객이 구매한 상품 중, 리뷰 평점이 4점 이상인 상품의 이름, 브랜드, 평균 평점, VIP 구매 횟
VIP 고객이 구매한 상품 중, 리뷰 평점이 4점 이상인 상품의 이름, 브랜드, 평균 평점, VIP 구매 횟수를 조회하세요. 상위 10개.
힌트 1: customers(grade='VIP') JOIN orders JOIN order_items JOIN products로 VIP 구매 상품을 파악. 서브쿼리로 리뷰 평균 4점 이상 필터링.
정답
SELECT
p.name, p.brand, review_avg.avg_rating,
COUNT(*) AS vip_purchase_count
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN products p ON oi.product_id = p.id
INNER JOIN (
SELECT product_id, ROUND(AVG(rating), 1) AS avg_rating
FROM reviews GROUP BY product_id HAVING AVG(rating) >= 4.0
) review_avg ON p.id = review_avg.product_id
WHERE c.grade = 'VIP'
GROUP BY p.id, p.name, p.brand, review_avg.avg_rating
ORDER BY vip_purchase_count DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| name | brand | avg_rating | vip_purchase_count |
|---|---|---|---|
| Crucial T700 2TB 실버 | Crucial | 4.20 | 617 |
| 로지텍 G502 X PLUS | 로지텍 | 4.20 | 429 |
| be quiet! Light Base 900 | be quiet! | 4.10 | 338 |
| Arctic Freezer 36 A-RGB 화이트 | Arctic | 4.00 | 317 |
| 로지텍 MX Anywhere 3S 블랙 | 로지텍 | 4.00 | 315 |
| 로지텍 G715 화이트 | 로지텍 | 4.10 | 314 |
| be quiet! Pure Power 12 M 850W 화이트 | be quiet! | 4.10 | 304 |
12. LEFT JOIN + COALESCE: 모든 상품의 이름, 가격, 총 판매 수량, 총 리뷰 수를 조회하세요.
LEFT JOIN + COALESCE: 모든 상품의 이름, 가격, 총 판매 수량, 총 리뷰 수를 조회하세요. 없으면 0. 상위 15개.
힌트 1: 카디널리티 폭발 방지를 위해 판매 수량과 리뷰 수를 각각 서브쿼리로 집계한 뒤 LEFT JOIN합니다.
정답
SELECT
p.name, p.price,
COALESCE(sales.total_qty, 0) AS total_sold,
COALESCE(rev.review_count, 0) AS review_count
FROM products p
LEFT JOIN (
SELECT product_id, SUM(quantity) AS total_qty
FROM order_items GROUP BY product_id
) sales ON p.id = sales.product_id
LEFT JOIN (
SELECT product_id, COUNT(*) AS review_count
FROM reviews GROUP BY product_id
) rev ON p.id = rev.product_id
WHERE p.is_active = 1
ORDER BY total_sold DESC
LIMIT 15;
실행 결과 (총 15행 중 상위 7행)
| name | price | total_sold | review_count |
|---|---|---|---|
| Crucial T700 2TB 실버 | 257,000.00 | 1503 | 77 |
| AMD Ryzen 9 9900X | 335,700.00 | 1447 | 65 |
| SK하이닉스 Platinum P41 2TB 실버 | 255,500.00 | 1359 | 49 |
| 로지텍 G502 X PLUS | 97,500.00 | 1087 | 101 |
| Kingston FURY Beast DDR4 16GB 실버 | 48,000.00 | 1061 | 102 |
| SteelSeries Prime Wireless 블랙 | 89,800.00 | 1034 | 80 |
| SteelSeries Aerox 5 Wireless 실버 | 100,000.00 | 1030 | 100 |
13. UNION + JOIN + GROUP BY: 고객별 "활동 점수"를 계산하세요. 주문=10점, 리뷰=5점,
UNION + JOIN + GROUP BY: 고객별 "활동 점수"를 계산하세요. 주문=10점, 리뷰=5점, 문의=3점. 상위 10명.
힌트 1: 각 활동별 점수를 UNION ALL로 합친 뒤, 외부에서 SUM으로 총점을 구합니다.
정답
SELECT
c.name, c.grade,
SUM(activity.score) AS total_score,
SUM(CASE WHEN activity.type = '주문' THEN 1 ELSE 0 END) AS orders,
SUM(CASE WHEN activity.type = '리뷰' THEN 1 ELSE 0 END) AS reviews,
SUM(CASE WHEN activity.type = '문의' THEN 1 ELSE 0 END) AS complaints
FROM (
SELECT customer_id, '주문' AS type, 10 AS score FROM orders
UNION ALL
SELECT customer_id, '리뷰' AS type, 5 AS score FROM reviews
UNION ALL
SELECT customer_id, '문의' AS type, 3 AS score FROM complaints
) activity
INNER JOIN customers c ON activity.customer_id = c.id
GROUP BY activity.customer_id, c.name, c.grade
ORDER BY total_score DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| name | grade | total_score | orders | reviews | complaints |
|---|---|---|---|---|---|
| 김병철 | VIP | 4095 | 366 | 72 | 25 |
| 박정수 | VIP | 3652 | 328 | 63 | 19 |
| 이영자 | VIP | 3467 | 307 | 62 | 29 |
| 강명자 | VIP | 3042 | 266 | 62 | 24 |
| 김성민 | VIP | 2794 | 246 | 59 | 13 |
| 정유진 | VIP | 2742 | 237 | 63 | 19 |
| 이미정 | VIP | 2697 | 234 | 60 | 19 |
14. 서브쿼리 + JOIN + 날짜: 가입 후 30일 이내/31~90일/90일 초과에 첫 주문을 한 고객 수를 비
서브쿼리 + JOIN + 날짜: 가입 후 30일 이내/31~90일/90일 초과에 첫 주문을 한 고객 수를 비교하세요.
힌트 1: MIN(ordered_at)으로 고객별 첫 주문일을 구한 서브쿼리를 만들고, julianday 차이로 일수를 계산합니다.
정답
SELECT
CASE
WHEN days_to_first_order <= 30 THEN '30일 이내'
WHEN days_to_first_order <= 90 THEN '31~90일'
ELSE '90일 초과'
END AS segment,
COUNT(*) AS customer_count,
ROUND(AVG(days_to_first_order), 1) AS avg_days
FROM (
SELECT c.id,
ROUND(julianday(MIN(o.ordered_at)) - julianday(c.created_at)) AS days_to_first_order
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.created_at
)
GROUP BY segment
ORDER BY MIN(days_to_first_order);
실행 결과 (3행)
| segment | customer_count | avg_days |
|---|---|---|
| 30일 이내 | 955 | 8.30 |
| 31~90일 | 596 | 56.10 |
| 90일 초과 | 1288 | 329.50 |
15. 결제 방법별, 카드 발급사별 매출 현황을 조회하세요. 카드 아닌 경우 발급사는 '해당없음'. 상위 15개.
결제 방법별, 카드 발급사별 매출 현황을 조회하세요. 카드 아닌 경우 발급사는 '해당없음'. 상위 15개.
힌트 1: COALESCE(card_issuer, '해당없음')으로 NULL 처리. GROUP BY method, card_issuer.
정답
SELECT
CASE pay.method
WHEN 'card' THEN '카드'
WHEN 'bank_transfer' THEN '계좌이체'
WHEN 'kakao_pay' THEN '카카오페이'
WHEN 'naver_pay' THEN '네이버페이'
ELSE pay.method
END AS method_kr,
COALESCE(pay.card_issuer, '해당없음') AS issuer,
COUNT(*) AS payment_count,
SUM(pay.amount) AS total_amount,
ROUND(AVG(pay.amount)) AS avg_amount
FROM payments pay
WHERE pay.status = 'completed'
GROUP BY pay.method, pay.card_issuer
ORDER BY total_amount DESC
LIMIT 15;
실행 결과 (총 14행 중 상위 7행)
| method_kr | issuer | payment_count | total_amount | avg_amount |
|---|---|---|---|---|
| 카카오페이 | 해당없음 | 6886 | 6,781,114,303.00 | 984,768.00 |
| 네이버페이 | 해당없음 | 5270 | 5,420,480,093.00 | 1,028,554.00 |
| 계좌이체 | 해당없음 | 3429 | 3,456,454,657.00 | 1,008,007.00 |
| 카드 | 신한카드 | 3084 | 3,079,917,150.00 | 998,676.00 |
| 카드 | KB국민카드 | 2230 | 2,428,560,078.00 | 1,089,040.00 |
| 카드 | 삼성카드 | 2386 | 2,323,039,558.00 | 973,613.00 |
| 카드 | 현대카드 | 1815 | 1,834,423,680.00 | 1,010,702.00 |
16. 고객을 최근 주문일 기준 RFM R등급으로 분류하세요. Active(30일), Warm(90일), Cold(
고객을 최근 주문일 기준 RFM R등급으로 분류하세요. Active(30일), Warm(90일), Cold(180일), Dormant.
힌트 1: LEFT JOIN으로 주문 없는 고객 포함. MAX(ordered_at)으로 최근 주문일 구한 뒤, julianday('now') - julianday(last_order)로 경과일 계산.
정답
SELECT
recency_group,
COUNT(*) AS customer_count,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER(), 1) AS pct
FROM (
SELECT c.id,
CASE
WHEN MAX(o.ordered_at) IS NULL THEN 'Dormant'
WHEN julianday('now') - julianday(MAX(o.ordered_at)) <= 30 THEN 'Active'
WHEN julianday('now') - julianday(MAX(o.ordered_at)) <= 90 THEN 'Warm'
WHEN julianday('now') - julianday(MAX(o.ordered_at)) <= 180 THEN 'Cold'
ELSE 'Dormant'
END AS recency_group
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id
)
GROUP BY recency_group
ORDER BY CASE recency_group WHEN 'Active' THEN 1 WHEN 'Warm' THEN 2 WHEN 'Cold' THEN 3 ELSE 4 END;
실행 결과 (2행)
| recency_group | customer_count | pct |
|---|---|---|
| Cold | 960 | 18.40 |
| Dormant | 4270 | 81.60 |
17. 월간 매출 리포트: 2024년 월별 매출, 주문 건수, 고유 고객 수, 건당 평균, 전월 대비 증감율.
월간 매출 리포트: 2024년 월별 매출, 주문 건수, 고유 고객 수, 건당 평균, 전월 대비 증감율.
힌트 1: LAG() 윈도우 함수로 전월 매출을 가져와 증감율을 계산합니다.
정답
SELECT
month, total_revenue, order_count, unique_customers,
ROUND(total_revenue * 1.0 / order_count) AS avg_order_value,
CASE
WHEN prev_revenue IS NULL THEN '-'
ELSE ROUND((total_revenue - prev_revenue) * 100.0 / prev_revenue, 1) || '%'
END AS growth_rate
FROM (
SELECT
SUBSTR(ordered_at, 1, 7) AS month,
SUM(total_amount) AS total_revenue,
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS unique_customers,
LAG(SUM(total_amount)) OVER (ORDER BY SUBSTR(ordered_at, 1, 7)) AS prev_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 | total_revenue | order_count | unique_customers | avg_order_value | growth_rate |
|---|---|---|---|---|---|
| 2024-01 | 301,075,320.00 | 325 | 275 | 926,386.00 | - |
| 2024-02 | 426,177,449.00 | 433 | 345 | 984,244.00 | 41.6% |
| 2024-03 | 536,322,767.00 | 572 | 428 | 937,627.00 | 25.8% |
| 2024-04 | 470,154,081.00 | 478 | 362 | 983,586.00 | -12.3% |
| 2024-05 | 459,724,596.00 | 396 | 323 | 1,160,921.00 | -2.2% |
| 2024-06 | 377,040,302.00 | 397 | 327 | 949,724.00 | -18.0% |
| 2024-07 | 363,944,597.00 | 391 | 320 | 930,805.00 | -3.5% |
18. 고객 등급별 평균 주문 금액, 평균 리뷰 평점, 반품율을 조회하세요.
고객 등급별 평균 주문 금액, 평균 리뷰 평점, 반품율을 조회하세요.
힌트 1: 각 지표를 서브쿼리로 따로 집계한 뒤 customers의 grade로 GROUP BY.
정답
SELECT
c.grade,
COUNT(DISTINCT c.id) AS customer_count,
ROUND(AVG(order_stats.avg_amount)) AS avg_order_amount,
ROUND(AVG(review_stats.avg_rating), 2) AS avg_rating,
ROUND(100.0 * SUM(COALESCE(return_stats.return_count, 0))
/ NULLIF(SUM(COALESCE(order_stats.order_count, 0)), 0), 2) AS return_rate_pct
FROM customers c
LEFT JOIN (
SELECT customer_id, COUNT(*) AS order_count, AVG(total_amount) AS avg_amount
FROM orders GROUP BY customer_id
) order_stats ON c.id = order_stats.customer_id
LEFT JOIN (
SELECT customer_id, AVG(rating) AS avg_rating
FROM reviews GROUP BY customer_id
) review_stats ON c.id = review_stats.customer_id
LEFT JOIN (
SELECT customer_id, COUNT(*) AS return_count
FROM returns GROUP BY customer_id
) return_stats ON c.id = return_stats.customer_id
GROUP BY c.grade
ORDER BY CASE c.grade WHEN 'VIP' THEN 1 WHEN 'GOLD' THEN 2 WHEN 'SILVER' THEN 3 WHEN 'BRONZE' THEN 4 END;
실행 결과 (4행)
| grade | customer_count | avg_order_amount | avg_rating | return_rate_pct |
|---|---|---|---|---|
| VIP | 368 | 1,419,393.00 | 3.88 | 2.56 |
| GOLD | 524 | 1,119,695.00 | 3.92 | 2.48 |
| SILVER | 479 | 887,476.00 | 3.90 | 2.68 |
| BRONZE | 3859 | 699,518.00 | 3.92 | 3.02 |
19. 재고 위험 상품 점검: 현재 재고가 최근 30일 판매량보다 적은 상품을 찾으세요. 예상 소진일 포함. 상위
재고 위험 상품 점검: 현재 재고가 최근 30일 판매량보다 적은 상품을 찾으세요. 예상 소진일 포함. 상위 15개.
힌트 1: 최근 30일 판매량은 order_items JOIN orders에서 ordered_at >= date('now', '-30 days')로 계산.
정답
SELECT
p.name, p.stock_qty AS current_stock,
COALESCE(sales.qty_30d, 0) AS sold_30d,
CASE
WHEN COALESCE(sales.qty_30d, 0) = 0 THEN '판매 없음'
ELSE CAST(ROUND(p.stock_qty * 30.0 / sales.qty_30d) AS INTEGER) || '일'
END AS est_days_left
FROM products p
LEFT JOIN (
SELECT oi.product_id, SUM(oi.quantity) AS qty_30d
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
WHERE o.ordered_at >= date('now', '-30 days') AND o.status NOT IN ('cancelled')
GROUP BY oi.product_id
) sales ON p.id = sales.product_id
WHERE p.is_active = 1 AND p.stock_qty < COALESCE(sales.qty_30d, 0)
ORDER BY p.stock_qty ASC
LIMIT 15;
20. 카테고리 계층 분석: 대분류(depth=0)별로 소속 상품 수, 총 매출, 평균 리뷰 평점을 조회하세요.
카테고리 계층 분석: 대분류(depth=0)별로 소속 상품 수, 총 매출, 평균 리뷰 평점을 조회하세요.
힌트 1: 카테고리의 parent_id를 따라 올라가야 합니다. categories를 자기 자신과 JOIN하여 대분류를 찾으세요.
정답
SELECT
top_cat.name AS top_category,
COUNT(DISTINCT p.id) AS product_count,
COALESCE(SUM(sales.revenue), 0) AS total_revenue,
ROUND(AVG(rev.avg_rating), 1) AS avg_rating
FROM categories top_cat
LEFT JOIN categories mid_cat ON mid_cat.parent_id = top_cat.id
LEFT JOIN categories sub_cat ON sub_cat.parent_id = mid_cat.id
LEFT JOIN products p ON p.category_id IN (top_cat.id, mid_cat.id, sub_cat.id)
LEFT JOIN (
SELECT product_id, SUM(subtotal) AS revenue FROM order_items GROUP BY product_id
) sales ON p.id = sales.product_id
LEFT JOIN (
SELECT product_id, AVG(rating) AS avg_rating FROM reviews GROUP BY product_id
) rev ON p.id = rev.product_id
WHERE top_cat.depth = 0
GROUP BY top_cat.id, top_cat.name
ORDER BY total_revenue DESC;
실행 결과 (총 18행 중 상위 7행)
| top_category | product_count | total_revenue | avg_rating |
|---|---|---|---|
| 노트북 | 29 | 10,050,178,200.00 | 3.70 |
| 그래픽카드 | 15 | 5,559,698,400.00 | 3.90 |
| 모니터 | 22 | 4,712,362,900.00 | 3.90 |
| 메인보드 | 23 | 3,225,292,000.00 | 3.70 |
| CPU | 7 | 1,858,019,600.00 | 3.80 |
| 스피커/헤드셋 | 12 | 1,546,414,500.00 | 3.90 |
| 저장장치 | 15 | 1,511,680,300.00 | 3.90 |
21. 공급업체 성과 비교: 공급업체별 공급 상품 수, 총 매출, 평균 리뷰 평점, 반품률. 매출 상위 10개.
공급업체 성과 비교: 공급업체별 공급 상품 수, 총 매출, 평균 리뷰 평점, 반품률. 매출 상위 10개.
힌트 1: 각 지표를 서브쿼리로 분리하면 깔끔합니다.
정답
SELECT
s.company_name,
COUNT(DISTINCT p.id) AS product_count,
COALESCE(SUM(sales.revenue), 0) AS total_revenue,
ROUND(AVG(rev.avg_rating), 1) AS avg_rating,
ROUND(100.0 * COALESCE(SUM(ret.return_count), 0)
/ NULLIF(COALESCE(SUM(sales.sold_count), 0), 0), 2) AS return_rate_pct
FROM suppliers s
INNER JOIN products p ON p.supplier_id = s.id
LEFT JOIN (SELECT product_id, SUM(subtotal) AS revenue, COUNT(*) AS sold_count FROM order_items GROUP BY product_id) sales ON p.id = sales.product_id
LEFT JOIN (SELECT product_id, AVG(rating) AS avg_rating FROM reviews GROUP BY product_id) rev ON p.id = rev.product_id
LEFT JOIN (SELECT oi.product_id, COUNT(DISTINCT r.id) AS return_count FROM returns r INNER JOIN orders o ON r.order_id = o.id INNER JOIN order_items oi ON o.id = oi.order_id GROUP BY oi.product_id) ret ON p.id = ret.product_id
GROUP BY s.id, s.company_name
ORDER BY total_revenue DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| company_name | product_count | total_revenue | avg_rating | return_rate_pct |
|---|---|---|---|---|
| 에이수스코리아 | 26 | 6,181,415,600.00 | 3.90 | 3.42 |
| 레이저코리아 | 9 | 4,167,252,100.00 | 3.90 | 4.17 |
| 삼성전자 공식 유통 | 26 | 3,000,828,600.00 | 3.80 | 2.96 |
| MSI코리아 | 13 | 2,787,039,100.00 | 3.90 | 3.50 |
| LG전자 공식 유통 | 11 | 2,221,778,900.00 | 3.90 | 3.61 |
| ASRock코리아 | 11 | 1,741,420,800.00 | 3.60 | 3.38 |
| 인텔코리아 | 6 | 1,267,419,000.00 | 3.80 | 3.21 |
22. 쿠폰 효과 분석: 쿠폰 사용/미사용 주문의 평균 결제금액, 평균 상품 수, 완료율을 비교하세요.
쿠폰 효과 분석: 쿠폰 사용/미사용 주문의 평균 결제금액, 평균 상품 수, 완료율을 비교하세요.
힌트 1: orders LEFT JOIN coupon_usage로 연결. CASE WHEN cu.id IS NOT NULL THEN '사용' ELSE '미사용' END로 구분.
정답
SELECT
CASE WHEN cu.id IS NOT NULL THEN '쿠폰 사용' ELSE '쿠폰 미사용' END AS coupon_group,
COUNT(DISTINCT o.id) AS order_count,
ROUND(AVG(o.total_amount)) AS avg_amount,
ROUND(AVG(item_stats.item_count), 1) AS avg_items,
ROUND(100.0 * SUM(CASE WHEN o.status = 'confirmed' THEN 1 ELSE 0 END)
/ COUNT(DISTINCT o.id), 1) AS confirm_rate_pct
FROM orders o
LEFT JOIN coupon_usage cu ON o.id = cu.order_id
LEFT JOIN (SELECT order_id, COUNT(*) AS item_count FROM order_items GROUP BY order_id) item_stats ON o.id = item_stats.order_id
GROUP BY CASE WHEN cu.id IS NOT NULL THEN '쿠폰 사용' ELSE '쿠폰 미사용' END;
실행 결과 (2행)
| coupon_group | order_count | avg_amount | avg_items | confirm_rate_pct |
|---|---|---|---|---|
| 쿠폰 미사용 | 35,893 | 998,277.00 | 2.40 | 91.20 |
| 쿠폰 사용 | 1664 | 1,413,663.00 | 2.90 | 100.00 |
23. 위시리스트 전환 분석: 카테고리별 위시리스트→구매 전환율을 조회하세요. 5건 이상만. 상위 10개.
위시리스트 전환 분석: 카테고리별 위시리스트→구매 전환율을 조회하세요. 5건 이상만. 상위 10개.
힌트 1: wishlists의 is_purchased 칼럼으로 전환 여부를 파악.
정답
SELECT
cat.name AS category,
COUNT(*) AS wishlist_count,
SUM(w.is_purchased) AS purchased_count,
ROUND(100.0 * SUM(w.is_purchased) / COUNT(*), 1) AS conversion_rate_pct
FROM wishlists w
INNER JOIN products p ON w.product_id = p.id
INNER JOIN categories cat ON p.category_id = cat.id
GROUP BY cat.id, cat.name
HAVING COUNT(*) >= 5
ORDER BY conversion_rate_pct DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| category | wishlist_count | purchased_count | conversion_rate_pct |
|---|---|---|---|
| 베어본 | 8 | 1 | 12.50 |
| 멤브레인 | 81 | 6 | 7.40 |
| 케이스 | 96 | 7 | 7.30 |
| Intel 소켓 | 92 | 5 | 5.40 |
| DDR5 | 64 | 3 | 4.70 |
| 유선 | 22 | 1 | 4.50 |
| AMD | 47 | 2 | 4.30 |
24. 고객 문의 응답 분석: 문의 유형별 평균 해결 시간, 해결률, 에스컬레이션 비율을 조회하세요.
고객 문의 응답 분석: 문의 유형별 평균 해결 시간, 해결률, 에스컬레이션 비율을 조회하세요.
힌트 1: julianday(resolved_at) - julianday(created_at)로 해결 시간. SUM(escalated)로 에스컬레이션 집계.
정답
SELECT
category,
COUNT(*) AS total_count,
ROUND(AVG(CASE WHEN resolved_at IS NOT NULL THEN julianday(resolved_at) - julianday(created_at) END), 1) AS avg_resolve_days,
ROUND(100.0 * SUM(CASE WHEN resolved_at IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*), 1) AS resolve_rate_pct,
ROUND(100.0 * SUM(escalated) / COUNT(*), 1) AS escalation_rate_pct
FROM complaints
GROUP BY category
ORDER BY total_count DESC;
실행 결과 (7행)
| category | total_count | avg_resolve_days | resolve_rate_pct | escalation_rate_pct |
|---|---|---|---|---|
| general_inquiry | 1232 | 1.70 | 94.20 | 4.20 |
| delivery_issue | 708 | 0.7 | 95.60 | 4.90 |
| refund_request | 522 | 0.8 | 94.40 | 11.10 |
| product_defect | 460 | 0.6 | 95.00 | 12.20 |
| price_inquiry | 439 | 1.60 | 95.70 | 3.20 |
| wrong_item | 240 | 0.6 | 94.20 | 6.70 |
| exchange_request | 212 | 1.30 | 95.30 | 14.60 |
25. 종합 대시보드 쿼리: 총 고객 수, 활성 고객 수, 이번 달/지난 달 매출, 평균 주문 금액, 평균 리뷰 평
종합 대시보드 쿼리: 총 고객 수, 활성 고객 수, 이번 달/지난 달 매출, 평균 주문 금액, 평균 리뷰 평점, 미해결 문의 건수를 한 번에 조회하세요.
힌트 1: 각 KPI를 SELECT '지표명' AS metric, 값 AS value 형태로 만들고 UNION ALL로 합칩니다.
정답
SELECT '총 고객 수' AS metric, CAST(COUNT(*) AS TEXT) AS value FROM customers
UNION ALL
SELECT '활성 고객 수', CAST(COUNT(*) AS TEXT) FROM customers WHERE is_active = 1
UNION ALL
SELECT '이번 달 매출', CAST(COALESCE(SUM(total_amount), 0) AS TEXT) FROM orders WHERE SUBSTR(ordered_at, 1, 7) = strftime('%Y-%m', 'now') AND status NOT IN ('cancelled')
UNION ALL
SELECT '지난 달 매출', CAST(COALESCE(SUM(total_amount), 0) AS TEXT) FROM orders WHERE SUBSTR(ordered_at, 1, 7) = strftime('%Y-%m', 'now', '-1 month') AND status NOT IN ('cancelled')
UNION ALL
SELECT '평균 주문 금액', CAST(ROUND(AVG(total_amount)) AS TEXT) FROM orders WHERE status NOT IN ('cancelled')
UNION ALL
SELECT '평균 리뷰 평점', CAST(ROUND(AVG(rating), 2) AS TEXT) FROM reviews
UNION ALL
SELECT '미해결 문의', CAST(COUNT(*) AS TEXT) FROM complaints WHERE status NOT IN ('resolved', 'closed');
실행 결과 (7행)
| metric | value |
|---|---|
| 총 고객 수 | 5230 |
| 활성 고객 수 | 3660 |
| 이번 달 매출 | 0 |
| 지난 달 매출 | 0 |
| 평균 주문 금액 | 1015193.0 |
| 평균 리뷰 평점 | 3.9 |
| 미해결 문의 | 197 |