콘텐츠로 이동

고객/운영 분석

사용 테이블

customers — 고객 (등급, 포인트, 가입채널)

orders — 주문 (상태, 금액, 일시)

order_items — 주문 상세 (수량, 단가)

products — 상품 (이름, 가격, 재고, 브랜드)

categories — 카테고리 (부모-자식 계층)

inventory_transactions — 재고 입출고 (유형, 수량)

customer_grade_history — 등급 이력 (변경 전후)

complaints — 고객 불만 (유형, 우선순위)

staff — 직원 (부서, 역할, 관리자)

학습 범위

RFM Analysis, Cohort, LTV, Inventory ABC Analysis, Safety Stock, CS Performance, CTE + Window Function

1. RFM 기초 — 고객별 핵심 지표 산출

마케팅팀이 고객 세분화를 위해 각 고객의 RFM 지표를 요청했습니다. 고객별 마지막 주문일(Recency), 총 주문 횟수(Frequency), 총 구매 금액(Monetary)을 구하세요. 취소/반품 주문은 제외합니다. 상위 20명만 표시합니다.

힌트 1: - Recency: MAX(ordered_at) - Frequency: COUNT(*) - Monetary: SUM(total_amount) - customers + orders JOIN

정답
SELECT
    c.id            AS customer_id,
    c.name          AS customer_name,
    c.grade,
    MAX(o.ordered_at)           AS last_order_date,
    COUNT(*)                    AS order_count,
    ROUND(SUM(o.total_amount))  AS total_spent
FROM customers AS c
INNER JOIN orders AS o ON c.id = o.customer_id
WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY c.id, c.name, c.grade
ORDER BY total_spent DESC
LIMIT 20;

실행 결과 (총 20행 중 상위 7행)

customer_id customer_name grade last_order_date order_count total_spent
226 박정수 VIP 2025-12-21 21:52:24 303 403,448,758.00
97 김병철 VIP 2025-12-28 11:37:58 342 366,385,931.00
162 강명자 VIP 2025-12-20 10:21:05 249 253,180,338.00
356 정유진 VIP 2025-10-24 16:44:53 223 244,604,910.00
549 이미정 VIP 2025-12-04 12:11:17 219 235,775,349.00
227 김성민 VIP 2025-12-19 22:54:22 230 234,708,853.00
98 이영자 VIP 2025-11-29 11:04:23 275 230,165,991.00

2. RFM 4분위 세그먼트

RFM 지표를 기반으로 고객을 4분위(Q1~Q4)로 나누세요. Recency는 최근일수록 높은 점수(4), Frequency와 Monetary는 클수록 높은 점수(4)입니다. 각 고객의 R/F/M 점수와 총점을 표시합니다.

힌트 1: - NTILE(4)로 각 지표를 4등분 - Recency: ORDER BY last_order_date ASC → NTILE 4가 가장 최근 - Frequency/Monetary: ORDER BY ... ASC → NTILE 4가 가장 큼 - 총점 = R + F + M (최대 12점)

정답
WITH rfm_raw AS (
    SELECT
        c.id AS customer_id,
        c.name,
        c.grade,
        MAX(o.ordered_at)           AS last_order_date,
        COUNT(*)                    AS frequency,
        ROUND(SUM(o.total_amount))  AS monetary
    FROM customers AS c
    INNER JOIN orders AS o ON c.id = o.customer_id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY c.id, c.name, c.grade
),
rfm_scored AS (
    SELECT
        customer_id,
        name,
        grade,
        last_order_date,
        frequency,
        monetary,
        NTILE(4) OVER (ORDER BY last_order_date ASC)  AS r_score,
        NTILE(4) OVER (ORDER BY frequency ASC)         AS f_score,
        NTILE(4) OVER (ORDER BY monetary ASC)          AS m_score
    FROM rfm_raw
)
SELECT
    customer_id,
    name,
    grade,
    r_score,
    f_score,
    m_score,
    r_score + f_score + m_score AS rfm_total,
    CASE
        WHEN r_score + f_score + m_score >= 10 THEN 'Champion'
        WHEN r_score + f_score + m_score >= 7  THEN 'Loyal'
        WHEN r_score >= 3 AND f_score <= 2     THEN 'New Customer'
        WHEN r_score <= 2 AND f_score >= 3     THEN 'At Risk'
        ELSE 'Regular'
    END AS segment
FROM rfm_scored
ORDER BY rfm_total DESC
LIMIT 20;

실행 결과 (총 20행 중 상위 7행)

customer_id name grade r_score f_score m_score rfm_total segment
486 김성호 GOLD 4 4 4 12 Champion
10 박지훈 GOLD 4 4 4 12 Champion
1490 이은영 VIP 4 4 4 12 Champion
647 김영희 VIP 4 4 4 12 Champion
1241 김정수 BRONZE 4 4 4 12 Champion
256 박준호 GOLD 4 4 4 12 Champion
2328 서성민 VIP 4 4 4 12 Champion

3. 코호트 리텐션 분석

2023년에 첫 구매한 고객의 월별 리텐션(재구매율)을 코호트 분석으로 구하세요. 가입 후 0개월(첫 구매), 1개월, 2개월, ... 6개월까지의 리텐션율을 계산합니다.

힌트 1: - 코호트 = 첫 구매 월이 같은 고객 그룹 - 월 차이 = 주문 월 - 첫 구매 월 (개월 수) - 리텐션율 = 해당 월에 주문한 코호트 고객 수 / 코호트 전체 고객 수

정답
WITH first_purchase AS (
    SELECT
        customer_id,
        SUBSTR(MIN(ordered_at), 1, 7) AS cohort_month
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY customer_id
    HAVING cohort_month LIKE '2023%'
),
monthly_activity AS (
    SELECT DISTINCT
        fp.customer_id,
        fp.cohort_month,
        SUBSTR(o.ordered_at, 1, 7) AS activity_month,
        (CAST(SUBSTR(o.ordered_at, 1, 4) AS INTEGER) - CAST(SUBSTR(fp.cohort_month, 1, 4) AS INTEGER)) * 12
        + CAST(SUBSTR(o.ordered_at, 6, 2) AS INTEGER) - CAST(SUBSTR(fp.cohort_month, 6, 2) AS INTEGER)
            AS month_offset
    FROM first_purchase AS fp
    INNER JOIN orders AS o ON fp.customer_id = o.customer_id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
),
cohort_size AS (
    SELECT cohort_month, COUNT(DISTINCT customer_id) AS total_customers
    FROM first_purchase
    GROUP BY cohort_month
)
SELECT
    ma.cohort_month,
    cs.total_customers,
    ma.month_offset,
    COUNT(DISTINCT ma.customer_id) AS active_customers,
    ROUND(100.0 * COUNT(DISTINCT ma.customer_id) / cs.total_customers, 1) AS retention_pct
FROM monthly_activity AS ma
INNER JOIN cohort_size AS cs ON ma.cohort_month = cs.cohort_month
WHERE ma.month_offset BETWEEN 0 AND 6
GROUP BY ma.cohort_month, cs.total_customers, ma.month_offset
ORDER BY ma.cohort_month, ma.month_offset;

실행 결과 (총 83행 중 상위 7행)

cohort_month total_customers month_offset active_customers retention_pct
2023-01 21 0 21 100.00
2023-01 21 1 2 9.50
2023-01 21 2 1 4.80
2023-01 21 3 2 9.50
2023-01 21 4 5 23.80
2023-01 21 5 7 33.30
2023-02 31 0 31 100.00

4. 이탈 고객 감지

마지막 주문 후 6개월 이상 구매가 없는 고객을 "이탈 위험"으로 분류하세요. 이탈 고객의 등급별 분포와 이탈 전 평균 구매 금액을 구합니다. (기준일: 2025-03-31)

힌트 1: - 이탈 기준: JULIANDAY('2025-03-31') - JULIANDAY(MAX(ordered_at)) > 180 - 등급별 GROUP BY - 이탈 고객과 활성 고객 비교도 함께 표시

정답
WITH customer_activity AS (
    SELECT
        c.id            AS customer_id,
        c.name,
        c.grade,
        MAX(o.ordered_at)           AS last_order_date,
        COUNT(*)                    AS order_count,
        ROUND(AVG(o.total_amount))  AS avg_order_value,
        ROUND(JULIANDAY('2025-03-31') - JULIANDAY(MAX(o.ordered_at))) AS days_since_last
    FROM customers AS c
    INNER JOIN orders AS o ON c.id = o.customer_id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY c.id, c.name, c.grade
)
SELECT
    grade,
    CASE WHEN days_since_last > 180 THEN '이탈 위험' ELSE '활성' END AS status,
    COUNT(*) AS customer_count,
    ROUND(AVG(avg_order_value)) AS avg_order_value,
    ROUND(AVG(order_count), 1)  AS avg_orders,
    ROUND(AVG(days_since_last)) AS avg_days_inactive
FROM customer_activity
GROUP BY grade,
    CASE WHEN days_since_last > 180 THEN '이탈 위험' ELSE '활성' END
ORDER BY
    CASE grade WHEN 'VIP' THEN 1 WHEN 'GOLD' THEN 2 WHEN 'SILVER' THEN 3 ELSE 4 END,
    status;

실행 결과 (5행)

grade status customer_count avg_order_value avg_orders avg_days_inactive
VIP 활성 368 1,437,751.00 38.20 -222.00
GOLD 활성 524 1,112,559.00 15.10 -169.00
SILVER 활성 479 877,935.00 10.70 -133.00
BRONZE 이탈 위험 599 840,118.00 4.50 578.00
BRONZE 활성 823 556,893.00 6.00 -69.00

5. 고객 생애 가치(LTV) 추정

고객 등급별 평균 LTV(Life Time Value)를 추정하세요. LTV = 평균 주문 금액 x 연간 주문 횟수 x 평균 활동 기간(년)

힌트 1: - 활동 기간 = (마지막 주문일 - 첫 주문일) / 365. 최소 1년으로 처리 - 연간 주문 횟수 = 총 주문 횟수 / 활동 기간 - 등급별 집계

정답
WITH customer_ltv AS (
    SELECT
        c.id,
        c.grade,
        COUNT(*) AS total_orders,
        AVG(o.total_amount) AS avg_order_value,
        MAX(JULIANDAY(o.ordered_at) - JULIANDAY(MIN(o.ordered_at)) OVER (PARTITION BY c.id))
            / 365.0 AS active_years_raw
    FROM customers AS c
    INNER JOIN orders AS o ON c.id = o.customer_id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY c.id, c.grade
),
ltv_calc AS (
    SELECT
        id,
        grade,
        total_orders,
        avg_order_value,
        MAX(active_years_raw, 1.0) AS active_years,
        total_orders / MAX(active_years_raw, 1.0) AS orders_per_year,
        avg_order_value * (total_orders / MAX(active_years_raw, 1.0)) * MAX(active_years_raw, 1.0)
            AS ltv
    FROM customer_ltv
)
SELECT
    grade,
    COUNT(*) AS customer_count,
    ROUND(AVG(avg_order_value))  AS avg_order_value,
    ROUND(AVG(orders_per_year), 1) AS avg_annual_orders,
    ROUND(AVG(active_years), 1)    AS avg_active_years,
    ROUND(AVG(ltv))                AS avg_ltv
FROM ltv_calc
GROUP BY grade
ORDER BY avg_ltv DESC;

6. 가입 채널별 고객 품질 비교

가입 채널(acquisition_channel)별로 고객 수, 평균 주문 횟수, 평균 구매 금액, VIP/GOLD 전환율을 비교하세요.

힌트 1: - customers.acquisition_channel: organic/search_ad/social/referral/direct - VIP/GOLD 전환율 = 해당 채널에서 VIP+GOLD 고객 수 / 전체 고객 수 - 주문이 없는 고객도 포함 (LEFT JOIN)

정답
WITH channel_stats AS (
    SELECT
        COALESCE(c.acquisition_channel, '미확인') AS channel,
        c.id AS customer_id,
        c.grade,
        COUNT(o.id) AS order_count,
        COALESCE(SUM(o.total_amount), 0) AS total_spent
    FROM customers AS c
    LEFT JOIN orders AS o
        ON c.id = o.customer_id
        AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY c.acquisition_channel, c.id, c.grade
)
SELECT
    channel,
    COUNT(*)                        AS customer_count,
    ROUND(AVG(order_count), 1)      AS avg_orders,
    ROUND(AVG(CASE WHEN total_spent > 0 THEN total_spent END)) AS avg_spent,
    ROUND(100.0 * SUM(CASE WHEN grade IN ('VIP', 'GOLD') THEN 1 ELSE 0 END)
        / COUNT(*), 1) AS premium_rate_pct,
    ROUND(100.0 * SUM(CASE WHEN order_count = 0 THEN 1 ELSE 0 END)
        / COUNT(*), 1) AS never_ordered_pct
FROM channel_stats
GROUP BY channel
ORDER BY avg_spent DESC;

실행 결과 (5행)

channel customer_count avg_orders avg_spent premium_rate_pct never_ordered_pct
organic 1146 8.00 14,523,151.00 19.00 45.90
direct 408 7.30 14,104,863.00 16.40 48.50
referral 708 6.80 12,712,480.00 16.80 45.80
search_ad 1543 6.40 11,875,351.00 17.30 45.70
social 1425 5.60 10,755,623.00 15.50 48.00

7. 등급 변동 추적

customer_grade_history 테이블을 활용하여, 2024년에 등급이 상승한 고객과 하락한 고객의 수를 구하세요. 등급 변동 경로(예: SILVER→GOLD)별 건수도 표시합니다.

힌트 1: - customer_grade_history: customer_id, old_grade, new_grade, changed_at - 등급 순서: BRONZE < SILVER < GOLD < VIP - CASE문으로 등급에 숫자 부여 후 비교

정답
WITH grade_order AS (
    SELECT
        customer_id,
        old_grade,
        new_grade,
        changed_at,
        CASE old_grade WHEN 'BRONZE' THEN 1 WHEN 'SILVER' THEN 2 WHEN 'GOLD' THEN 3 WHEN 'VIP' THEN 4 END AS old_rank,
        CASE new_grade WHEN 'BRONZE' THEN 1 WHEN 'SILVER' THEN 2 WHEN 'GOLD' THEN 3 WHEN 'VIP' THEN 4 END AS new_rank
    FROM customer_grade_history
    WHERE changed_at LIKE '2024%'
)
SELECT
    old_grade || ' → ' || new_grade AS grade_change,
    CASE
        WHEN new_rank > old_rank THEN '승급'
        WHEN new_rank < old_rank THEN '강등'
        ELSE '유지'
    END AS direction,
    COUNT(*) AS change_count
FROM grade_order
GROUP BY old_grade, new_grade, direction
ORDER BY
    CASE WHEN new_rank > old_rank THEN 1 WHEN new_rank < old_rank THEN 2 ELSE 3 END,
    change_count DESC;

실행 결과 (총 13행 중 상위 7행)

grade_change direction change_count
BRONZE → GOLD 승급 167
BRONZE → SILVER 승급 160
BRONZE → VIP 승급 66
GOLD → VIP 승급 63
SILVER → GOLD 승급 52
SILVER → VIP 승급 37
SILVER → BRONZE 강등 125

8. ABC 재고 분류

현재 재고 금액(stock_qty x cost_price) 기준으로 상품을 ABC 분류하세요. (A: 상위 70%, B: 70~90%, C: 나머지) 각 등급의 상품 수와 총 재고 금액을 표시합니다.

힌트 1: - 재고 금액 = stock_qty * cost_price - 누적 비율로 ABC 분류 (매출 분석의 ABC와 동일 패턴) - 활성 상품만 대상

정답
WITH inventory_value AS (
    SELECT
        p.id,
        p.name,
        p.stock_qty,
        p.cost_price,
        p.stock_qty * p.cost_price AS stock_value
    FROM products AS p
    WHERE p.is_active = 1
      AND p.stock_qty > 0
),
cumulative AS (
    SELECT
        id, name, stock_qty, cost_price, stock_value,
        SUM(stock_value) OVER (ORDER BY stock_value DESC) AS cum_value,
        SUM(stock_value) OVER () AS total_value
    FROM inventory_value
),
classified AS (
    SELECT *,
        CASE
            WHEN 100.0 * cum_value / total_value <= 70 THEN 'A'
            WHEN 100.0 * cum_value / total_value <= 90 THEN 'B'
            ELSE 'C'
        END AS abc_class
    FROM cumulative
)
SELECT
    abc_class,
    COUNT(*)                 AS product_count,
    ROUND(SUM(stock_value)) AS total_stock_value,
    ROUND(100.0 * SUM(stock_value) / (SELECT SUM(stock_value) FROM inventory_value), 1) AS value_pct,
    ROUND(AVG(stock_qty))    AS avg_stock_qty
FROM classified
GROUP BY abc_class
ORDER BY abc_class;

실행 결과 (3행)

abc_class product_count total_stock_value value_pct avg_stock_qty
A 46 20,873,678,400.00 69.50 337.00
B 52 6,146,023,700.00 20.50 310.00
C 119 3,010,558,600.00 10.00 233.00

9. 체류 재고(Dead Stock) 감지

최근 6개월간 한 건도 판매되지 않은 활성 상품을 찾으세요. 재고 수량, 재고 금액, 마지막 판매일을 표시합니다.

힌트 1: - order_items + orders에서 최근 6개월 주문 확인 - LEFT JOIN 후 주문이 NULL인 상품 = 판매 없음 - 또는 NOT EXISTS 패턴 활용

정답
WITH last_sale AS (
    SELECT
        oi.product_id,
        MAX(o.ordered_at) AS last_sold_at
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY oi.product_id
)
SELECT
    p.name         AS product_name,
    cat.name       AS category,
    p.stock_qty,
    ROUND(p.stock_qty * p.cost_price) AS stock_value,
    ls.last_sold_at,
    ROUND(JULIANDAY('2025-03-31') - JULIANDAY(ls.last_sold_at)) AS days_since_last_sale
FROM products AS p
INNER JOIN categories AS cat ON p.category_id = cat.id
LEFT JOIN last_sale AS ls ON p.id = ls.product_id
WHERE p.is_active = 1
  AND p.stock_qty > 0
  AND (ls.last_sold_at IS NULL OR ls.last_sold_at < DATE('2025-03-31', '-6 months'))
ORDER BY stock_value DESC
LIMIT 20;

10. 재주문 시점(Reorder Point) 계산

상품별 일평균 출고량을 기반으로 재주문 시점을 계산하세요. 재주문 시점 = 일평균 출고량 x 리드타임(7일) x 안전 계수(1.5) 현재 재고가 재주문 시점 이하인 상품을 찾습니다.

힌트 1: - 최근 3개월 출고량: inventory_transactions에서 type = 'outbound' - 일평균 출고량 = 3개월 총 출고량 / 90 - 재주문 시점 = 일평균 출고량 x 7 x 1.5

정답
WITH daily_demand AS (
    SELECT
        product_id,
        ABS(SUM(quantity)) / 90.0 AS avg_daily_demand
    FROM inventory_transactions
    WHERE type = 'outbound'
      AND created_at >= DATE('2025-03-31', '-3 months')
    GROUP BY product_id
),
reorder_calc AS (
    SELECT
        p.id,
        p.name,
        p.stock_qty,
        ROUND(dd.avg_daily_demand, 2) AS avg_daily_demand,
        ROUND(dd.avg_daily_demand * 7 * 1.5) AS reorder_point,
        ROUND(p.stock_qty / NULLIF(dd.avg_daily_demand, 0)) AS days_of_stock
    FROM products AS p
    INNER JOIN daily_demand AS dd ON p.id = dd.product_id
    WHERE p.is_active = 1
)
SELECT
    name AS product_name,
    stock_qty,
    avg_daily_demand,
    reorder_point,
    days_of_stock,
    CASE
        WHEN stock_qty <= reorder_point THEN '즉시 발주'
        WHEN days_of_stock <= 14 THEN '발주 예정'
        ELSE '충분'
    END AS order_status
FROM reorder_calc
WHERE stock_qty <= reorder_point
ORDER BY days_of_stock ASC;

실행 결과 (1행)

product_name stock_qty avg_daily_demand reorder_point days_of_stock order_status
Arctic Freezer 36 A-RGB 화이트 0 0.09 1.00 0.0 즉시 발주

11. 카테고리별 재고 회전율

카테고리별 재고 회전율을 계산하세요. 재고 회전율 = 연간 매출원가(COGS) / 평균 재고 금액

힌트 1: - 매출원가(COGS): SUM(oi.quantity * p.cost_price) (2024년 판매분) - 평균 재고 금액: AVG(stock_qty * cost_price) (현재 시점) - 회전율이 높을수록 재고가 빠르게 팔림

정답
WITH cogs_2024 AS (
    SELECT
        p.category_id,
        SUM(oi.quantity * p.cost_price) AS annual_cogs
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    INNER JOIN products AS p ON oi.product_id = p.id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY p.category_id
),
avg_inventory AS (
    SELECT
        category_id,
        AVG(stock_qty * cost_price) AS avg_stock_value,
        SUM(stock_qty * cost_price) AS total_stock_value,
        COUNT(*) AS product_count
    FROM products
    WHERE is_active = 1
    GROUP BY category_id
)
SELECT
    cat.name AS category,
    ai.product_count,
    ROUND(cg.annual_cogs) AS annual_cogs,
    ROUND(ai.total_stock_value) AS current_stock_value,
    ROUND(cg.annual_cogs / NULLIF(ai.avg_stock_value, 0), 1) AS turnover_rate,
    ROUND(365.0 / NULLIF(cg.annual_cogs / NULLIF(ai.avg_stock_value, 0), 0)) AS days_in_inventory
FROM avg_inventory AS ai
INNER JOIN cogs_2024 AS cg ON ai.category_id = cg.category_id
INNER JOIN categories AS cat ON ai.category_id = cat.id
ORDER BY turnover_rate DESC;

실행 결과 (총 38행 중 상위 7행)

category product_count annual_cogs current_stock_value turnover_rate days_in_inventory
유선 2 10,751,300.00 1,612,200.00 13.30 27.00
케이스 10 103,892,500.00 261,356,300.00 4.00 92.00
스피커/헤드셋 9 164,632,200.00 416,492,100.00 3.60 103.00
게이밍 8 76,210,600.00 187,113,000.00 3.30 112.00
DDR5 8 115,931,100.00 308,598,300.00 3.00 121.00
DDR4 5 41,246,500.00 69,961,100.00 2.90 124.00
SSD 6 122,676,700.00 258,240,800.00 2.90 128.00

12. 재고 입출고 월별 추이

2024년 월별 입고량, 출고량, 순변동량, 월말 누적 재고를 계산하세요. 전체 상품 합산 기준입니다.

힌트 1: - inventory_transactionstype: inbound(양수), outbound(음수) - 조건부 집계로 입고/출고 분리 - 누적 합: SUM(...) OVER (ORDER BY year_month)

정답
WITH monthly_flow AS (
    SELECT
        SUBSTR(created_at, 1, 7) AS year_month,
        SUM(CASE WHEN quantity > 0 THEN quantity ELSE 0 END) AS inbound_qty,
        SUM(CASE WHEN quantity < 0 THEN ABS(quantity) ELSE 0 END) AS outbound_qty,
        SUM(quantity) AS net_change
    FROM inventory_transactions
    WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
    GROUP BY SUBSTR(created_at, 1, 7)
)
SELECT
    year_month,
    inbound_qty,
    outbound_qty,
    net_change,
    SUM(net_change) OVER (ORDER BY year_month) AS cumulative_change
FROM monthly_flow
ORDER BY year_month;

실행 결과 (총 12행 중 상위 7행)

year_month inbound_qty outbound_qty net_change cumulative_change
2024-01 2007 199 1808 1808
2024-02 943 163 780 2588
2024-03 3536 248 3288 5876
2024-04 3528 122 3406 9282
2024-05 3408 253 3155 12,437
2024-06 2688 171 2517 14,954
2024-07 2900 138 2762 17,716

13. 문의 처리 시간 분석

2024년 문의 유형(category)별 평균 처리 시간, 중간값(근사), SLA 준수율을 구하세요. SLA: 일반 문의 3일, 클레임 1일, 긴급 0.5일 이내 해결

힌트 1: - 처리 시간: JULIANDAY(resolved_at) - JULIANDAY(created_at) - 중간값 근사: 50번째 백분위수 → PERCENTILE 대신 NTILE 활용 - SLA 기준을 priority에 따라 다르게 적용

정답
WITH resolution_times AS (
    SELECT
        category,
        priority,
        JULIANDAY(resolved_at) - JULIANDAY(created_at) AS resolution_days,
        CASE priority
            WHEN 'urgent' THEN 0.5
            WHEN 'high'   THEN 1.0
            WHEN 'medium' THEN 2.0
            ELSE 3.0
        END AS sla_days
    FROM complaints
    WHERE created_at LIKE '2024%'
      AND resolved_at IS NOT NULL
)
SELECT
    category,
    COUNT(*) AS resolved_count,
    ROUND(AVG(resolution_days), 2) AS avg_resolution_days,
    ROUND(MIN(resolution_days), 2) AS min_days,
    ROUND(MAX(resolution_days), 2) AS max_days,
    ROUND(100.0 * SUM(CASE WHEN resolution_days <= sla_days THEN 1 ELSE 0 END)
        / COUNT(*), 1) AS sla_compliance_pct
FROM resolution_times
GROUP BY category
ORDER BY sla_compliance_pct ASC;

실행 결과 (7행)

category resolved_count avg_resolution_days min_days max_days sla_compliance_pct
general_inquiry 195 1.92 0.04 4.00 76.90
exchange_request 34 1.25 0.08 3.96 88.20
price_inquiry 73 1.62 0.13 3.79 90.40
delivery_issue 109 0.73 0.04 4.00 95.40
refund_request 64 0.91 0.04 3.46 96.90
wrong_item 40 0.78 0.08 3.21 97.50
product_defect 69 0.7 0.04 3.46 98.60

14. 반품 사유 분석과 추이

2024년 반품 사유(reason)별 건수, 비율, 평균 환불 금액을 구하세요. 분기별 추이도 함께 확인합니다.

힌트 1: - returns 테이블: reason, refund_amount, requested_at - 전체 비율 + 분기별 추이를 별도 쿼리로 작성

정답
-- 사유별 전체 통계
SELECT
    reason,
    COUNT(*)                     AS return_count,
    ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct,
    ROUND(AVG(refund_amount))    AS avg_refund,
    ROUND(SUM(refund_amount))    AS total_refund
FROM returns
WHERE requested_at >= '2024-01-01' AND requested_at < '2025-01-01'
GROUP BY reason
ORDER BY return_count DESC;

-- 분기별 추이
SELECT
    'Q' || ((CAST(SUBSTR(requested_at, 6, 2) AS INTEGER) + 2) / 3) AS quarter,
    reason,
    COUNT(*) AS return_count
FROM returns
WHERE requested_at >= '2024-01-01' AND requested_at < '2025-01-01'
GROUP BY (CAST(SUBSTR(requested_at, 6, 2) AS INTEGER) + 2) / 3, reason
ORDER BY quarter, return_count DESC;

15. CS 직원별 성과 비교

각 CS 직원의 담당 문의 건수, 해결률, 평균 처리 시간, 고객 만족도(compensation 없는 비율)를 비교하세요. 전체 평균과의 차이도 함께 표시합니다.

힌트 1: - complaints.staff_idstaff JOIN - 윈도우 함수 AVG(...) OVER ()로 전체 평균을 같은 행에 표시 - 고객 만족도 대리 지표: compensation_type이 NULL 또는 'none'인 비율

정답
WITH staff_metrics AS (
    SELECT
        s.name AS staff_name,
        COUNT(*) AS case_count,
        SUM(CASE WHEN comp.status IN ('resolved', 'closed') THEN 1 ELSE 0 END) AS resolved_count,
        AVG(CASE
            WHEN comp.resolved_at IS NOT NULL
            THEN JULIANDAY(comp.resolved_at) - JULIANDAY(comp.created_at)
        END) AS avg_resolution_days,
        100.0 * SUM(CASE WHEN COALESCE(comp.compensation_type, 'none') = 'none' THEN 1 ELSE 0 END)
            / COUNT(*) AS no_compensation_pct
    FROM complaints AS comp
    INNER JOIN staff AS s ON comp.staff_id = s.id
    WHERE comp.created_at LIKE '2024%'
    GROUP BY s.id, s.name
)
SELECT
    staff_name,
    case_count,
    ROUND(100.0 * resolved_count / case_count, 1) AS resolution_rate,
    ROUND(avg_resolution_days, 2) AS avg_days,
    ROUND(no_compensation_pct, 1) AS satisfaction_proxy_pct,
    ROUND(AVG(case_count) OVER (), 1) AS team_avg_cases,
    ROUND(AVG(avg_resolution_days) OVER (), 2) AS team_avg_days
FROM staff_metrics
ORDER BY resolution_rate DESC;

실행 결과 (5행)

staff_name case_count resolution_rate avg_days satisfaction_proxy_pct team_avg_cases team_avg_days
이준혁 123 95.10 1.36 67.50 123.20 1.29
박경수 118 94.90 1.29 60.20 123.20 1.29
장주원 115 94.80 1.32 63.50 123.20 1.29
한민재 131 94.70 1.19 57.30 123.20 1.29
권영희 129 94.60 1.29 72.10 123.20 1.29

16. 에스컬레이션 분석

에스컬레이션(escalated = 1)된 문의의 특성을 분석하세요. 어떤 유형, 채널, 우선순위에서 에스컬레이션이 많이 발생하는지 파악합니다.

힌트 1: - complaints.escalated = 1인 건 - 에스컬레이션 비율 = 에스컬레이션 건수 / 전체 건수 - 카테고리, 채널, 우선순위별로 각각 집계

정답
SELECT
    category,
    channel,
    priority,
    COUNT(*) AS total_count,
    SUM(escalated) AS escalated_count,
    ROUND(100.0 * SUM(escalated) / COUNT(*), 1) AS escalation_rate_pct,
    ROUND(AVG(CASE
        WHEN escalated = 1 AND resolved_at IS NOT NULL
        THEN JULIANDAY(resolved_at) - JULIANDAY(created_at)
    END), 2) AS escalated_avg_days
FROM complaints
WHERE created_at LIKE '2024%'
GROUP BY category, channel, priority
HAVING COUNT(*) >= 5
ORDER BY escalation_rate_pct DESC
LIMIT 15;

실행 결과 (총 15행 중 상위 7행)

category channel priority total_count escalated_count escalation_rate_pct escalated_avg_days
wrong_item website high 8 3 37.50 0.4
product_defect website medium 8 2 25.00 0.92
refund_request phone medium 9 2 22.20 0.6
product_defect email medium 5 1 20.00 0.33
product_defect phone urgent 5 1 20.00 0.13
wrong_item phone medium 5 1 20.00 1.04
exchange_request website medium 6 1 16.70 1.33

17. 상품별 CS 발생률

매출 상위 30개 상품의 CS(문의+반품) 발생률을 구하세요. CS 발생률이 높은 상품은 품질 개선이 필요합니다.

힌트 1: - 판매 건수 대비 문의/반품 발생 비율 - order_items에서 상품별 판매 건수 - complaints + returns에서 상품 관련 CS 건수 - complaints는 order_id → order_items → product_id로 연결

정답
WITH top_products AS (
    SELECT
        p.id AS product_id,
        p.name AS product_name,
        SUM(oi.quantity) AS total_sold
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    INNER JOIN products AS p ON oi.product_id = p.id
    WHERE o.status NOT IN ('cancelled')
      AND o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
    GROUP BY p.id, p.name
    ORDER BY total_sold DESC
    LIMIT 30
),
complaint_counts AS (
    SELECT
        oi.product_id,
        COUNT(DISTINCT comp.id) AS complaint_count
    FROM complaints AS comp
    INNER JOIN orders AS o ON comp.order_id = o.id
    INNER JOIN order_items AS oi ON o.id = oi.order_id
    WHERE comp.created_at LIKE '2024%'
    GROUP BY oi.product_id
),
return_counts AS (
    SELECT
        oi.product_id,
        COUNT(DISTINCT ret.id) AS return_count
    FROM returns AS ret
    INNER JOIN order_items AS oi ON ret.order_id = oi.order_id
    WHERE ret.requested_at LIKE '2024%'
    GROUP BY oi.product_id
)
SELECT
    tp.product_name,
    tp.total_sold,
    COALESCE(cc.complaint_count, 0) AS complaints,
    COALESCE(rc.return_count, 0) AS returns,
    COALESCE(cc.complaint_count, 0) + COALESCE(rc.return_count, 0) AS total_cs,
    ROUND(100.0 * (COALESCE(cc.complaint_count, 0) + COALESCE(rc.return_count, 0))
        / tp.total_sold, 1) AS cs_rate_pct
FROM top_products AS tp
LEFT JOIN complaint_counts AS cc ON tp.product_id = cc.product_id
LEFT JOIN return_counts AS rc ON tp.product_id = rc.product_id
ORDER BY cs_rate_pct DESC;

실행 결과 (총 30행 중 상위 7행)

product_name total_sold complaints returns total_cs cs_rate_pct
SK하이닉스 Platinum P41 2TB 실버 183 22 9 31 16.90
Crucial T700 2TB 실버 173 17 9 26 15.00
Intel Core Ultra 7 265K 화이트 405 33 18 51 12.60
로지텍 G715 240 21 9 30 12.50
Kingston FURY Beast DDR4 16GB 실버 170 15 6 21 12.40
Kingston FURY Renegade DDR5 32GB 7200... 131 14 2 16 12.20
AMD Ryzen 9 9900X 248 21 9 30 12.10

18. 일간 운영 대시보드

특정 날짜(2024-12-15)의 운영 현황을 한눈에 보여주는 대시보드를 만드세요. 당일 주문 수, 매출, 신규 가입자, 배송 완료, 미해결 CS 건수를 포함합니다.

힌트 1: - 각 지표를 스칼라 서브쿼리 또는 CTE로 계산 - CROSS JOIN으로 단일 행 결합 - 전일 대비 변화도 포함하면 더 유용

정답
WITH target_day AS (SELECT '2024-12-15' AS d),
day_orders AS (
    SELECT
        COUNT(*) AS order_count,
        ROUND(SUM(total_amount)) AS revenue
    FROM orders, target_day
    WHERE DATE(ordered_at) = d
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
),
prev_orders AS (
    SELECT
        COUNT(*) AS order_count,
        ROUND(SUM(total_amount)) AS revenue
    FROM orders, target_day
    WHERE DATE(ordered_at) = DATE(d, '-1 day')
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
),
new_customers AS (
    SELECT COUNT(*) AS cnt
    FROM customers, target_day
    WHERE DATE(created_at) = d
),
deliveries AS (
    SELECT COUNT(*) AS cnt
    FROM shipping, target_day
    WHERE DATE(delivered_at) = d
),
open_cs AS (
    SELECT COUNT(*) AS cnt
    FROM complaints
    WHERE status = 'open'
)
SELECT
    (SELECT d FROM target_day) AS report_date,
    do.order_count AS today_orders,
    do.revenue AS today_revenue,
    po.order_count AS yesterday_orders,
    po.revenue AS yesterday_revenue,
    ROUND(100.0 * (do.revenue - po.revenue) / NULLIF(po.revenue, 0), 1) AS revenue_change_pct,
    nc.cnt AS new_signups,
    dl.cnt AS deliveries_completed,
    oc.cnt AS open_cs_tickets
FROM day_orders AS do
CROSS JOIN prev_orders AS po
CROSS JOIN new_customers AS nc
CROSS JOIN deliveries AS dl
CROSS JOIN open_cs AS oc;

실행 결과 (1행)

report_date today_orders today_revenue yesterday_orders yesterday_revenue revenue_change_pct new_signups deliveries_completed open_cs_tickets
2024-12-15 17 14,671,908.00 16 8,477,723.00 73.10 0 15 197

19. 공급업체 종합 평가

각 공급업체의 종합 성과를 평가하세요. 공급 상품 수, 총 매출, 반품률, 평균 리뷰 평점, 재고 적정성을 하나의 리포트로 통합합니다.

힌트 1: - suppliersproducts → 각종 테이블 JOIN - 반품률: 해당 공급업체 상품의 반품 수 / 판매 수 - 재고 적정성: 재고 과다(180일 이상 분량) 또는 부족(7일 이하)인 상품 비율

정답
WITH supplier_products AS (
    SELECT
        s.id AS supplier_id,
        s.company_name,
        COUNT(DISTINCT p.id) AS product_count,
        SUM(p.stock_qty) AS total_stock
    FROM suppliers AS s
    INNER JOIN products AS p ON s.id = p.supplier_id
    WHERE p.is_active = 1
    GROUP BY s.id, s.company_name
),
supplier_sales AS (
    SELECT
        p.supplier_id,
        SUM(oi.quantity) AS total_sold,
        ROUND(SUM(oi.quantity * oi.unit_price)) AS total_revenue
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    INNER JOIN products AS p ON oi.product_id = p.id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY p.supplier_id
),
supplier_returns AS (
    SELECT
        p.supplier_id,
        COUNT(DISTINCT ret.id) AS return_count
    FROM returns AS ret
    INNER JOIN order_items AS oi ON ret.order_id = oi.order_id
    INNER JOIN products AS p ON oi.product_id = p.id
    WHERE ret.requested_at >= '2024-01-01' AND ret.requested_at < '2025-01-01'
    GROUP BY p.supplier_id
),
supplier_reviews AS (
    SELECT
        p.supplier_id,
        ROUND(AVG(r.rating), 2) AS avg_rating,
        COUNT(*) AS review_count
    FROM reviews AS r
    INNER JOIN products AS p ON r.product_id = p.id
    GROUP BY p.supplier_id
)
SELECT
    sp.company_name,
    sp.product_count,
    COALESCE(ss.total_revenue, 0) AS revenue_2024,
    COALESCE(ss.total_sold, 0) AS units_sold,
    ROUND(100.0 * COALESCE(sr.return_count, 0) / NULLIF(ss.total_sold, 0), 1) AS return_rate_pct,
    COALESCE(srv.avg_rating, 0) AS avg_rating,
    sp.total_stock
FROM supplier_products AS sp
LEFT JOIN supplier_sales AS ss ON sp.supplier_id = ss.supplier_id
LEFT JOIN supplier_returns AS sr ON sp.supplier_id = sr.supplier_id
LEFT JOIN supplier_reviews AS srv ON sp.supplier_id = srv.supplier_id
ORDER BY revenue_2024 DESC;

실행 결과 (총 41행 중 상위 7행)

company_name product_count revenue_2024 units_sold return_rate_pct avg_rating total_stock
에이수스코리아 21 794,354,100.00 828 4.00 3.88 5828
레이저코리아 7 482,608,700.00 445 3.60 3.89 1742
LG전자 공식 유통 11 434,068,900.00 389 3.90 3.91 2667
삼성전자 공식 유통 21 427,788,200.00 1012 3.50 3.90 6174
MSI코리아 12 372,284,800.00 632 3.00 3.97 4070
ASRock코리아 9 238,530,100.00 537 3.20 3.78 3084
스틸시리즈코리아 7 209,711,600.00 983 3.70 3.87 1626

20. 월간 경영 종합 리포트 (2024년 12월)

2024년 12월의 경영 종합 리포트를 생성하세요. 매출, 고객, 재고, CS 네 영역의 핵심 KPI를 하나의 쿼리로 통합합니다.

KPI 영역 지표
매출 월 매출, 전월비, 전년 동월비
고객 활성 고객 수, 신규 가입, 재구매율
재고 품절 상품 수, 재고 금액, 재고 부족 경고 수
CS 미해결 건수, 평균 처리 시간, 해결률

힌트 1: - 각 영역을 별도 CTE로 작성한 뒤 CROSS JOIN - 전월비/전년 동월비는 서브쿼리로 비교 기간 매출 계산 - 재구매율: 해당 월 주문 고객 중 이전에도 주문한 고객 비율

정답
WITH sales_kpi AS (
    SELECT
        ROUND(SUM(CASE WHEN ordered_at LIKE '2024-12%' THEN total_amount ELSE 0 END)) AS dec_revenue,
        ROUND(SUM(CASE WHEN ordered_at LIKE '2024-11%' THEN total_amount ELSE 0 END)) AS nov_revenue,
        ROUND(SUM(CASE WHEN ordered_at LIKE '2023-12%' THEN total_amount ELSE 0 END)) AS dec_2023_revenue
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
      AND (ordered_at LIKE '2024-12%' OR ordered_at LIKE '2024-11%' OR ordered_at LIKE '2023-12%')
),
customer_kpi AS (
    SELECT
        COUNT(DISTINCT CASE WHEN o.ordered_at LIKE '2024-12%' THEN o.customer_id END) AS active_customers,
        (SELECT COUNT(*) FROM customers WHERE created_at LIKE '2024-12%') AS new_signups,
        ROUND(100.0 * COUNT(DISTINCT CASE
            WHEN o.ordered_at LIKE '2024-12%'
            AND o.customer_id IN (
                SELECT customer_id FROM orders
                WHERE ordered_at < '2024-12-01'
                  AND status NOT IN ('cancelled', 'returned', 'return_requested')
            )
            THEN o.customer_id
        END) / NULLIF(COUNT(DISTINCT CASE WHEN o.ordered_at LIKE '2024-12%' THEN o.customer_id END), 0), 1)
            AS repeat_rate_pct
    FROM orders AS o
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
),
inventory_kpi AS (
    SELECT
        SUM(CASE WHEN stock_qty = 0 THEN 1 ELSE 0 END) AS out_of_stock_count,
        ROUND(SUM(stock_qty * cost_price)) AS total_stock_value,
        SUM(CASE WHEN stock_qty > 0 AND stock_qty <= 10 THEN 1 ELSE 0 END) AS low_stock_warning
    FROM products
    WHERE is_active = 1
),
cs_kpi AS (
    SELECT
        SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) AS open_tickets,
        ROUND(AVG(CASE
            WHEN resolved_at IS NOT NULL AND created_at LIKE '2024-12%'
            THEN JULIANDAY(resolved_at) - JULIANDAY(created_at)
        END), 2) AS avg_resolution_days,
        ROUND(100.0 * SUM(CASE
            WHEN created_at LIKE '2024-12%' AND status IN ('resolved', 'closed') THEN 1 ELSE 0 END)
            / NULLIF(SUM(CASE WHEN created_at LIKE '2024-12%' THEN 1 ELSE 0 END), 0), 1)
            AS resolution_rate_pct
    FROM complaints
)
SELECT
    '2024-12' AS report_month,
    -- 매출
    sk.dec_revenue,
    ROUND(100.0 * (sk.dec_revenue - sk.nov_revenue) / NULLIF(sk.nov_revenue, 0), 1) AS mom_growth_pct,
    ROUND(100.0 * (sk.dec_revenue - sk.dec_2023_revenue) / NULLIF(sk.dec_2023_revenue, 0), 1) AS yoy_growth_pct,
    -- 고객
    ck.active_customers,
    ck.new_signups,
    ck.repeat_rate_pct,
    -- 재고
    ik.out_of_stock_count,
    ik.total_stock_value,
    ik.low_stock_warning,
    -- CS
    csk.open_tickets,
    csk.avg_resolution_days,
    csk.resolution_rate_pct
FROM sales_kpi AS sk
CROSS JOIN customer_kpi AS ck
CROSS JOIN inventory_kpi AS ik
CROSS JOIN cs_kpi AS csk;

실행 결과 (1행)

report_month dec_revenue mom_growth_pct yoy_growth_pct active_customers new_signups repeat_rate_pct out_of_stock_count total_stock_value low_stock_warning open_tickets avg_resolution_days resolution_rate_pct
2024-12 417,148,762.00 -23.20 14.90 379 59 90.20 1 30,030,260,700.00 2 197 1.39 91.70