CS 성과 분석
사용 테이블
complaints — 고객 불만 (유형, 우선순위)
staff — 직원 (부서, 역할, 관리자)
customers — 고객 (등급, 포인트, 가입채널)
orders — 주문 (상태, 금액, 일시)
returns — 반품/교환 (사유, 상태)
학습 범위
JULIANDAY, CASE, Window Functions, CTE, Pivot
1. 문의 유형별 현황
CS팀장이 최근 1년(2025년) 문의 현황을 파악하고 싶어합니다. 문의 유형(category)별로 건수, 해결률, 평균 처리 시간을 보여주세요.
힌트 1: - complaints 테이블의 category, status, created_at, resolved_at 사용
- 해결률: status IN ('resolved', 'closed') 건수 / 전체 건수
- 처리 시간: JULIANDAY(resolved_at) - JULIANDAY(created_at)
정답
SELECT
category,
COUNT(*) AS total_count,
SUM(CASE WHEN status IN ('resolved', 'closed') THEN 1 ELSE 0 END)
AS resolved_count,
ROUND(100.0 * SUM(CASE WHEN status IN ('resolved', 'closed') THEN 1 ELSE 0 END)
/ COUNT(*), 1) AS resolution_rate_pct,
ROUND(AVG(CASE
WHEN resolved_at IS NOT NULL
THEN JULIANDAY(resolved_at) - JULIANDAY(created_at)
END), 1) AS avg_resolution_days
FROM complaints
WHERE created_at LIKE '2025%'
GROUP BY category
ORDER BY total_count DESC;
실행 결과 (7행)
| category | total_count | resolved_count | resolution_rate_pct | avg_resolution_days |
|---|---|---|---|---|
| general_inquiry | 378 | 356 | 94.20 | 2.00 |
| price_inquiry | 140 | 129 | 92.10 | 1.80 |
| delivery_issue | 135 | 130 | 96.30 | 0.7 |
| refund_request | 92 | 87 | 94.60 | 0.7 |
| product_defect | 74 | 70 | 94.60 | 0.7 |
| wrong_item | 42 | 41 | 97.60 | 0.4 |
| exchange_request | 34 | 34 | 100.00 | 1.20 |
2. CS 직원별 성과 비교
각 직원이 담당한 문의 건수, 해결률, 평균 처리 시간, 담당 고객 수를 보여주세요. 전체 평균과 비교할 수 있도록 표시합니다.
힌트 1: - complaints.staff_id -> staff JOIN
- staff_id가 NULL인 문의는 "미배정"으로 표시
- 윈도우 함수로 전체 평균을 같은 행에 표시
정답
WITH staff_metrics AS (
SELECT
COALESCE(s.name, '미배정') AS staff_name,
COUNT(*) AS case_count,
COUNT(DISTINCT comp.customer_id) AS customer_count,
SUM(CASE WHEN comp.status IN ('resolved', 'closed') THEN 1 ELSE 0 END)
AS resolved_count,
ROUND(100.0 * SUM(CASE WHEN comp.status IN ('resolved', 'closed') THEN 1 ELSE 0 END)
/ COUNT(*), 1) AS resolution_rate,
ROUND(AVG(CASE
WHEN comp.resolved_at IS NOT NULL
THEN JULIANDAY(comp.resolved_at) - JULIANDAY(comp.created_at)
END), 1) AS avg_days
FROM complaints AS comp
LEFT JOIN staff AS s ON comp.staff_id = s.id
WHERE comp.created_at LIKE '2025%'
GROUP BY COALESCE(s.name, '미배정')
)
SELECT
staff_name,
case_count,
customer_count,
resolution_rate,
avg_days,
ROUND(AVG(case_count) OVER (), 0) AS team_avg_cases,
ROUND(AVG(resolution_rate) OVER (), 1) AS team_avg_rate,
ROUND(AVG(avg_days) OVER (), 1) AS team_avg_days
FROM staff_metrics
ORDER BY resolution_rate DESC;
실행 결과 (5행)
| staff_name | case_count | customer_count | resolution_rate | avg_days | team_avg_cases | team_avg_rate | team_avg_days |
|---|---|---|---|---|---|---|---|
| 한민재 | 178 | 167 | 96.10 | 1.30 | 179.00 | 94.60 | 1.40 |
| 장주원 | 183 | 178 | 95.60 | 1.50 | 179.00 | 94.60 | 1.40 |
| 권영희 | 177 | 174 | 94.90 | 1.60 | 179.00 | 94.60 | 1.40 |
| 이준혁 | 183 | 175 | 93.40 | 1.30 | 179.00 | 94.60 | 1.40 |
| 박경수 | 174 | 168 | 93.10 | 1.50 | 179.00 | 94.60 | 1.40 |
3. 반품 사유 분석
반품 사유별 건수, 비율, 평균 환불 금액을 보여주세요. 카테고리별로 어떤 사유가 많은지도 분석합니다.
힌트 1: - returns.reason으로 사유 분류
- returns -> orders -> order_items -> products -> categories JOIN
- returns.refund_amount로 환불 금액 계산
정답
SELECT
r.reason,
COUNT(*) AS return_count,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct,
ROUND(AVG(r.refund_amount), 2) AS avg_refund
FROM returns AS r
GROUP BY r.reason
ORDER BY return_count DESC;
실행 결과 (6행)
| reason | return_count | pct | avg_refund |
|---|---|---|---|
| change_of_mind | 343 | 34.30 | 1,160,924.02 |
| defective | 263 | 26.30 | 1,278,164.64 |
| damaged_in_transit | 165 | 16.50 | 1,043,268.48 |
| wrong_item | 95 | 9.50 | 1,128,795.79 |
| not_as_described | 88 | 8.80 | 1,290,557.95 |
| late_delivery | 46 | 4.60 | 741,530.43 |
4. 월별 CS 트렌드와 주문 대비 문의 비율
2024년 월별 주문 수, 문의 수, 반품 수, 주문 대비 문의율(%)을 보여주세요.
힌트 1: - 각 테이블에서 월별 건수를 CTE로 준비 - 월(SUBSTR)로 JOIN - 문의율 = 문의 수 / 주문 수 * 100
정답
WITH monthly_orders AS (
SELECT
SUBSTR(ordered_at, 1, 7) AS year_month,
COUNT(*) AS order_count
FROM orders
WHERE ordered_at LIKE '2024%'
GROUP BY SUBSTR(ordered_at, 1, 7)
),
monthly_complaints AS (
SELECT
SUBSTR(created_at, 1, 7) AS year_month,
COUNT(*) AS complaint_count
FROM complaints
WHERE created_at LIKE '2024%'
GROUP BY SUBSTR(created_at, 1, 7)
),
monthly_returns AS (
SELECT
SUBSTR(requested_at, 1, 7) AS year_month,
COUNT(*) AS return_count
FROM returns
WHERE requested_at LIKE '2024%'
GROUP BY SUBSTR(requested_at, 1, 7)
)
SELECT
mo.year_month,
mo.order_count,
COALESCE(mc.complaint_count, 0) AS complaint_count,
COALESCE(mr.return_count, 0) AS return_count,
ROUND(100.0 * COALESCE(mc.complaint_count, 0) / mo.order_count, 2)
AS complaint_rate_pct,
ROUND(100.0 * COALESCE(mr.return_count, 0) / mo.order_count, 2)
AS return_rate_pct
FROM monthly_orders AS mo
LEFT JOIN monthly_complaints AS mc ON mo.year_month = mc.year_month
LEFT JOIN monthly_returns AS mr ON mo.year_month = mr.year_month
ORDER BY mo.year_month;
실행 결과 (총 12행 중 상위 7행)
| year_month | order_count | complaint_count | return_count | complaint_rate_pct | return_rate_pct |
|---|---|---|---|---|---|
| 2024-01 | 346 | 43 | 9 | 12.43 | 2.60 |
| 2024-02 | 465 | 40 | 14 | 8.60 | 3.01 |
| 2024-03 | 601 | 50 | 23 | 8.32 | 3.83 |
| 2024-04 | 506 | 61 | 12 | 12.06 | 2.37 |
| 2024-05 | 415 | 47 | 10 | 11.33 | 2.41 |
| 2024-06 | 415 | 49 | 11 | 11.81 | 2.65 |
| 2024-07 | 414 | 49 | 6 | 11.84 | 1.45 |
5. 반복 문의 고객과 에스컬레이션 대상
3회 이상 문의한 고객 중 미해결 건이 있는 경우를 에스컬레이션 대상으로 분류하세요. 총 문의 수, 미해결 건수, 총 구매 금액, 마지막 문의일을 보여주세요.
힌트 1: - 고객별 문의 통계를 먼저 집계
- HAVING으로 3회 이상 + 미해결 건 존재 필터
- orders와 JOIN하여 구매 금액 포함
정답
WITH complaint_summary AS (
SELECT
customer_id,
COUNT(*) AS total_complaints,
SUM(CASE WHEN status NOT IN ('resolved', 'closed') THEN 1 ELSE 0 END)
AS open_count,
MAX(created_at) AS last_complaint_date
FROM complaints
GROUP BY customer_id
HAVING COUNT(*) >= 3
AND SUM(CASE WHEN status NOT IN ('resolved', 'closed') THEN 1 ELSE 0 END) > 0
),
customer_value AS (
SELECT
customer_id,
ROUND(SUM(total_amount), 2) AS total_spent
FROM orders
WHERE status NOT IN ('cancelled')
GROUP BY customer_id
)
SELECT
c.name AS customer_name,
c.grade,
c.email,
cs.total_complaints,
cs.open_count,
cs.last_complaint_date,
COALESCE(cv.total_spent, 0) AS total_spent,
CASE
WHEN c.grade IN ('VIP', 'GOLD') THEN '우선 처리'
WHEN cs.open_count >= 3 THEN '긴급'
ELSE '일반'
END AS priority
FROM complaint_summary AS cs
INNER JOIN customers AS c ON cs.customer_id = c.id
LEFT JOIN customer_value AS cv ON cs.customer_id = cv.customer_id
ORDER BY
CASE
WHEN c.grade IN ('VIP', 'GOLD') THEN 1
WHEN cs.open_count >= 3 THEN 2
ELSE 3
END,
cv.total_spent DESC;
실행 결과 (총 91행 중 상위 7행)
| customer_name | grade | total_complaints | open_count | last_complaint_date | total_spent | priority | |
|---|---|---|---|---|---|---|---|
| 강명자 | VIP | user162@testmail.kr | 24 | 1 | 2025-03-25 05:20:31 | 254,525,838.00 | 우선 처리 |
| 정유진 | VIP | user356@testmail.kr | 19 | 2 | 2024-08-28 10:20:59 | 248,498,783.00 | 우선 처리 |
| 이영자 | VIP | user98@testmail.kr | 29 | 1 | 2024-10-07 08:05:39 | 248,168,491.00 | 우선 처리 |
| 김성민 | VIP | user227@testmail.kr | 13 | 1 | 2025-05-10 11:44:37 | 244,859,844.00 | 우선 처리 |
| 김영길 | GOLD | user1581@testmail.kr | 11 | 1 | 2025-08-30 11:40:39 | 208,621,108.00 | 우선 처리 |
| 이영철 | VIP | user33@testmail.kr | 11 | 1 | 2023-08-06 03:15:06 | 174,223,341.00 | 우선 처리 |
| 김민재 | VIP | user3@testmail.kr | 9 | 1 | 2025-12-09 09:46:23 | 164,856,056.00 | 우선 처리 |
6. 보너스: CS 직원별 월별 성과 피벗
CS 직원별 2024년 월별 해결 건수를 피벗 테이블로 만드세요. 12개월(열) x 직원(행) 형태입니다.
힌트 1: - 12개 SUM(CASE WHEN SUBSTR(...) = 'MM' AND status ... THEN 1 ELSE 0 END) 칼럼
- LEFT JOIN staff으로 미배정 포함
정답
SELECT
COALESCE(s.name, '미배정') AS staff_name,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='01' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS jan,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='02' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS feb,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='03' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS mar,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='04' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS apr,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='05' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS may,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='06' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS jun,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='07' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS jul,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='08' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS aug,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='09' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS sep,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='10' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS oct,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='11' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS nov,
SUM(CASE WHEN SUBSTR(comp.created_at,6,2)='12' AND comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS dec,
SUM(CASE WHEN comp.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS total
FROM complaints AS comp
LEFT JOIN staff AS s ON comp.staff_id = s.id
WHERE comp.created_at LIKE '2024%'
GROUP BY COALESCE(s.name, '미배정')
ORDER BY total DESC;
실행 결과 (5행)
| staff_name | jan | feb | mar | apr | may | jun | jul | aug | sep | oct | nov | dec | total |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 한민재 | 7 | 9 | 11 | 11 | 12 | 6 | 15 | 14 | 8 | 8 | 14 | 9 | 124 |
| 권영희 | 9 | 8 | 12 | 12 | 8 | 10 | 8 | 7 | 17 | 13 | 13 | 5 | 122 |
| 이준혁 | 8 | 10 | 6 | 9 | 8 | 11 | 7 | 14 | 13 | 13 | 8 | 10 | 117 |
| 박경수 | 6 | 10 | 6 | 14 | 7 | 9 | 10 | 10 | 9 | 10 | 12 | 9 | 112 |
| 장주원 | 12 | 2 | 10 | 13 | 10 | 11 | 8 | 4 | 12 | 6 | 10 | 11 | 109 |