콘텐츠로 이동

CTE 활용

사용 테이블

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

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

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

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

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

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

calendar — 날짜 차원 (요일, 공휴일)

학습 범위

WITH, WITH RECURSIVE, Multiple CTE Chaining, CTE + Aggregate, CTE + JOIN, CTE + Window Function

1. 월별 매출과 평균 비교

2024년 월별 매출을 CTE로 구하고, 전체 평균 매출 이상인 월만 조회하세요.

힌트 1: CTE에서 월별 매출을 집계한 뒤, 외부 쿼리에서 WHERE revenue >= (SELECT AVG(revenue) FROM cte명)으로 필터링합니다. CTE는 여러 번 참조할 수 있습니다.

정답
WITH monthly_revenue AS (
    SELECT
        SUBSTR(ordered_at, 1, 7) AS year_month,
        ROUND(SUM(total_amount), 0) AS revenue,
        COUNT(*) AS order_count
    FROM orders
    WHERE ordered_at LIKE '2024%'
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY SUBSTR(ordered_at, 1, 7)
)
SELECT
    year_month,
    revenue,
    order_count,
    (SELECT ROUND(AVG(revenue), 0) FROM monthly_revenue) AS avg_revenue,
    CASE
        WHEN revenue >= (SELECT AVG(revenue) FROM monthly_revenue) THEN '평균 이상'
        ELSE '평균 미만'
    END AS status
FROM monthly_revenue
WHERE revenue >= (SELECT AVG(revenue) FROM monthly_revenue)
ORDER BY revenue DESC;

실행 결과 (4행)

year_month revenue order_count avg_revenue status
2024-11 543,313,372.00 547 426,203,627.00 평균 이상
2024-09 536,079,841.00 523 426,203,627.00 평균 이상
2024-03 519,844,502.00 555 426,203,627.00 평균 이상
2024-04 451,877,581.00 466 426,203,627.00 평균 이상

2. 고객별 총 구매 금액을 CTE로 구한 뒤, 등급별 상위 5명씩 추출하세요.

CTE와 ROW_NUMBER 윈도우 함수를 조합합니다.

힌트 1: 첫 번째 CTE에서 고객별 총 구매 금액을 집계하고, 두 번째 CTE에서 ROW_NUMBER() OVER (PARTITION BY grade ORDER BY total_spent DESC)를 적용합니다.

정답
WITH customer_totals AS (
    SELECT
        c.id,
        c.name,
        c.grade,
        ROUND(SUM(o.total_amount), 0) AS total_spent,
        COUNT(*) AS order_count
    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
),
ranked AS (
    SELECT
        *,
        ROW_NUMBER() OVER (
            PARTITION BY grade
            ORDER BY total_spent DESC
        ) AS rn
    FROM customer_totals
)
SELECT
    grade,
    rn AS rank_in_grade,
    name,
    order_count,
    total_spent
FROM ranked
WHERE rn <= 5
ORDER BY
    CASE grade
        WHEN 'VIP' THEN 1
        WHEN 'GOLD' THEN 2
        WHEN 'SILVER' THEN 3
        WHEN 'BRONZE' THEN 4
    END,
    rn;

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

grade rank_in_grade name order_count total_spent
VIP 1 박정수 303 403,448,758.00
VIP 2 김병철 342 366,385,931.00
VIP 3 강명자 249 253,180,338.00
VIP 4 정유진 223 244,604,910.00
VIP 5 이미정 219 235,775,349.00
GOLD 1 김경희 171 204,611,811.00
GOLD 2 김영길 160 199,282,408.00

3. CTE를 이용해 상품별 매출과 리뷰 요약을 한 번에 조회하세요.

2024년 매출 상위 10개 상품의 매출액, 판매량, 리뷰 수, 평균 평점을 표시합니다.

힌트 1: 매출 CTE와 리뷰 CTE를 별도로 만든 뒤, 외부 쿼리에서 LEFT JOIN으로 합칩니다. 리뷰가 없는 상품도 포함되어야 합니다.

정답
WITH sales AS (
    SELECT
        oi.product_id,
        ROUND(SUM(oi.quantity * oi.unit_price), 0) AS total_revenue,
        SUM(oi.quantity) AS units_sold
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    WHERE o.ordered_at LIKE '2024%'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY oi.product_id
),
review_summary AS (
    SELECT
        product_id,
        COUNT(*) AS review_count,
        ROUND(AVG(rating), 2) AS avg_rating
    FROM reviews
    GROUP BY product_id
)
SELECT
    p.name          AS product_name,
    cat.name        AS category,
    s.units_sold,
    s.total_revenue,
    COALESCE(r.review_count, 0) AS review_count,
    r.avg_rating
FROM sales AS s
INNER JOIN products   AS p   ON s.product_id  = p.id
INNER JOIN categories AS cat ON p.category_id = cat.id
LEFT  JOIN review_summary AS r ON s.product_id = r.product_id
ORDER BY s.total_revenue DESC
LIMIT 10;

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

product_name category units_sold total_revenue review_count avg_rating
Razer Blade 18 블랙 게이밍 노트북 38 165,417,800.00 20 4.10
Razer Blade 16 실버 게이밍 노트북 37 137,007,300.00 19 3.95
MacBook Air 15 M3 실버 맥북 23 126,065,300.00 4 3.75
ASUS Dual RTX 4060 Ti 블랙 NVIDIA 40 106,992,000.00 16 3.75
ASUS Dual RTX 5070 Ti 실버 NVIDIA 106 104,558,400.00 23 3.65
ASUS ROG Swift PG32UCDM 실버 게이밍 모니터 48 90,734,400.00 24 3.67
ASUS ROG Strix Scar 16 게이밍 노트북 35 85,837,500.00 13 4.23

4. CTE에서 집계한 결과를 다른 CTE에서 필터링하는 체이닝을 연습하세요.

월별 신규 가입자 수를 구하고, 전월 대비 증감률이 20% 이상인 "급증" 월을 찾으세요.

힌트 1: CTE 1: 월별 신규 가입자 수 집계. CTE 2: LAG로 전월 수치를 가져와 증감률 계산. 외부 쿼리에서 증감률 >= 20인 행만 필터링합니다.

정답
WITH monthly_signups AS (
    SELECT
        SUBSTR(created_at, 1, 7) AS year_month,
        COUNT(*) AS signup_count
    FROM customers
    WHERE created_at >= '2022-01-01'
    GROUP BY SUBSTR(created_at, 1, 7)
),
with_growth AS (
    SELECT
        year_month,
        signup_count,
        LAG(signup_count, 1) OVER (ORDER BY year_month) AS prev_count,
        ROUND(100.0 * (signup_count - LAG(signup_count, 1) OVER (ORDER BY year_month))
            / LAG(signup_count, 1) OVER (ORDER BY year_month), 1) AS growth_pct
    FROM monthly_signups
)
SELECT
    year_month,
    signup_count,
    prev_count,
    growth_pct
FROM with_growth
WHERE growth_pct >= 20
ORDER BY growth_pct DESC;

실행 결과 (6행)

year_month signup_count prev_count growth_pct
2024-06 68 43 58.10
2024-03 71 48 47.90
2025-10 76 54 40.70
2022-03 59 42 40.50
2024-10 65 51 27.50
2025-07 66 55 20.00

5. 카테고리별 매출 비중을 CTE로 구하고, 누적 비중(파레토)을 계산하세요.

2024년 기준 카테고리별 매출을 구하고, 비중 내림차순으로 누적 비중을 표시합니다.

힌트 1: CTE에서 카테고리별 매출을 집계한 뒤, SUM(share_pct) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING)으로 누적 비중을 구합니다.

정답
WITH category_revenue AS (
    SELECT
        cat.name AS category,
        ROUND(SUM(oi.quantity * oi.unit_price), 0) AS 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
    INNER JOIN categories AS cat ON p.category_id = cat.id
    WHERE o.ordered_at LIKE '2024%'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY cat.name
),
with_share AS (
    SELECT
        category,
        revenue,
        ROUND(100.0 * revenue / SUM(revenue) OVER (), 1) AS share_pct
    FROM category_revenue
)
SELECT
    category,
    revenue,
    share_pct,
    ROUND(SUM(share_pct) OVER (
        ORDER BY revenue DESC
        ROWS UNBOUNDED PRECEDING
    ), 1) AS cumulative_pct
FROM with_share
ORDER BY revenue DESC;

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

category revenue share_pct cumulative_pct
게이밍 노트북 636,925,700.00 12.30 12.30
AMD 447,953,400.00 8.70 21.00
게이밍 모니터 353,934,400.00 6.90 27.90
NVIDIA 345,858,700.00 6.70 34.60
2in1 340,884,400.00 6.60 41.20
일반 노트북 291,760,500.00 5.70 46.90
전문가용 모니터 254,590,200.00 4.90 51.80

6. 카테고리 계층 구조를 재귀 CTE로 전개하세요.

최상위 카테고리(parent_id IS NULL)부터 하위 카테고리까지, 전체 경로를 표시합니다.

힌트 1: WITH RECURSIVE를 사용합니다. 앵커 멤버: WHERE parent_id IS NULL. 재귀 멤버: CTE 자신과 categoriesc.parent_id = cte.id로 JOIN합니다. 경로는 parent_path || ' > ' || c.name으로 연결합니다.

정답
WITH RECURSIVE cat_tree AS (
    -- 앵커: 최상위 카테고리
    SELECT
        id,
        parent_id,
        name,
        name AS full_path,
        depth,
        0 AS level
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- 재귀: 자식 카테고리
    SELECT
        c.id,
        c.parent_id,
        c.name,
        ct.full_path || ' > ' || c.name,
        c.depth,
        ct.level + 1
    FROM categories AS c
    INNER JOIN cat_tree AS ct ON c.parent_id = ct.id
)
SELECT
    id,
    full_path,
    level,
    depth
FROM cat_tree
ORDER BY full_path;

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

id full_path level depth
14 CPU 0 0
16 CPU > AMD 1 1
15 CPU > Intel 1 1
49 UPS/전원 0 0
27 그래픽카드 0 0
29 그래픽카드 > AMD 1 1
28 그래픽카드 > NVIDIA 1 1

7. 직원 조직도를 재귀 CTE로 전개하세요.

대표(manager_id IS NULL)부터 시작하여 모든 직원의 직속 상관 경로를 표시합니다.

힌트 1: staff 테이블의 manager_id가 자기 참조(Self-Join)입니다. 앵커: WHERE manager_id IS NULL (최상위 관리자). 재귀: s.manager_id = tree.id로 부하 직원을 연결합니다.

정답
WITH RECURSIVE org_tree AS (
    -- 앵커: 최고 관리자
    SELECT
        id,
        name,
        department,
        role,
        manager_id,
        name AS chain,
        0 AS depth
    FROM staff
    WHERE manager_id IS NULL

    UNION ALL

    -- 재귀: 부하 직원
    SELECT
        s.id,
        s.name,
        s.department,
        s.role,
        s.manager_id,
        ot.chain || ' > ' || s.name,
        ot.depth + 1
    FROM staff AS s
    INNER JOIN org_tree AS ot ON s.manager_id = ot.id
)
SELECT
    id,
    name,
    department,
    role,
    depth,
    chain
FROM org_tree
ORDER BY chain;

실행 결과 (5행)

id name department role depth chain
1 한민재 경영 admin 0 한민재
3 박경수 경영 admin 1 한민재 > 박경수
5 권영희 마케팅 manager 2 한민재 > 박경수 > 권영희
4 이준혁 영업 manager 1 한민재 > 이준혁
2 장주원 경영 admin 1 한민재 > 장주원

8. 각 관리자 아래의 총 부하 직원 수를 재귀 CTE로 구하세요.

직접 부하뿐 아니라 간접 부하(부하의 부하)까지 포함하는 전체 팀 크기를 계산합니다.

힌트 1: 재귀 CTE로 모든 상하 관계를 전개한 뒤, 각 상위 관리자의 id를 루트로 기록합니다. 루트별로 COUNT(*) - 1 (자기 자신 제외)을 구하면 됩니다.

정답
WITH RECURSIVE subordinates AS (
    -- 앵커: 각 직원을 자기 자신의 루트로 시작
    SELECT
        id AS root_id,
        id AS member_id
    FROM staff
    WHERE is_active = 1

    UNION ALL

    -- 재귀: 부하 직원 추가
    SELECT
        sub.root_id,
        s.id
    FROM staff AS s
    INNER JOIN subordinates AS sub ON s.manager_id = sub.member_id
    WHERE s.is_active = 1
)
SELECT
    s.id,
    s.name,
    s.department,
    s.role,
    COUNT(*) - 1 AS total_subordinates
FROM subordinates AS sub
INNER JOIN staff AS s ON sub.root_id = s.id
GROUP BY s.id, s.name, s.department, s.role
HAVING COUNT(*) > 1
ORDER BY total_subordinates DESC;

실행 결과 (2행)

id name department role total_subordinates
1 한민재 경영 admin 4
3 박경수 경영 admin 1

9. 다중 CTE로 고객의 RFM(Recency, Frequency, Monetary) 세그먼트를 구축하세요.

3개의 CTE를 체이닝합니다: (1) RFM 원시 지표, (2) NTILE 점수, (3) 세그먼트 라벨링.

힌트 1: CTE 1(rfm_raw): MAX(ordered_at), COUNT(*), SUM(total_amount). CTE 2(rfm_scored): NTILE(4) 적용. CTE 3 또는 외부 쿼리: R+F+M 합산 점수로 세그먼트(VIP/충성/일반/이탈위험)를 부여합니다.

정답
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), 0) 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
        *,
        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
),
rfm_segment AS (
    SELECT
        *,
        r_score + f_score + m_score AS total_score,
        CASE
            WHEN r_score + f_score + m_score >= 10 THEN 'Champions'
            WHEN r_score + f_score + m_score >= 8  THEN 'Loyal'
            WHEN r_score + f_score + m_score >= 5  THEN 'Potential'
            ELSE 'At Risk'
        END AS segment
    FROM rfm_scored
)
SELECT
    segment,
    COUNT(*)                     AS customer_count,
    ROUND(AVG(frequency), 1)     AS avg_frequency,
    ROUND(AVG(monetary), 0)      AS avg_monetary,
    ROUND(AVG(r_score + f_score + m_score), 1) AS avg_score
FROM rfm_segment
GROUP BY segment
ORDER BY avg_score DESC;

실행 결과 (4행)

segment customer_count avg_frequency avg_monetary avg_score
Champions 784 32.90 34,334,528.00 11.00
Loyal 569 8.50 8,532,740.00 8.40
Potential 999 3.40 2,797,314.00 6.00
At Risk 441 1.50 456,944.00 3.50

10. 카테고리 트리별 매출 집계

재귀 CTE로 카테고리 트리를 전개한 뒤, 각 최상위 카테고리별 소속 상품 수와 매출을 집계하세요.

힌트 1: 재귀 CTE로 각 카테고리의 최상위 루트를 추적합니다. 앵커에서 id AS root_id, name AS root_name을 기록하고, 재귀에서 부모의 root_id, root_name을 그대로 전파합니다. 결과에 products, order_items, orders를 JOIN하여 루트별 집계합니다.

정답
WITH RECURSIVE cat_root AS (
    -- 앵커: 최상위 카테고리 (자기 자신이 루트)
    SELECT
        id,
        id   AS root_id,
        name AS root_name
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- 재귀: 자식 카테고리는 부모의 루트를 상속
    SELECT
        c.id,
        cr.root_id,
        cr.root_name
    FROM categories AS c
    INNER JOIN cat_root AS cr ON c.parent_id = cr.id
)
SELECT
    cr.root_name                  AS top_category,
    COUNT(DISTINCT p.id)          AS product_count,
    ROUND(SUM(oi.quantity * oi.unit_price), 0) AS total_revenue,
    SUM(oi.quantity)              AS units_sold
FROM cat_root AS cr
INNER JOIN products   AS p  ON p.category_id = cr.id
INNER JOIN order_items AS oi ON oi.product_id = p.id
INNER JOIN orders     AS o  ON oi.order_id   = o.id
WHERE o.ordered_at LIKE '2024%'
  AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY cr.root_id, cr.root_name
ORDER BY total_revenue DESC;

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

top_category product_count total_revenue units_sold
노트북 21 1,395,635,900.00 660
모니터 19 727,065,300.00 712
그래픽카드 10 713,579,800.00 580
메인보드 16 398,988,900.00 920
스피커/헤드셋 8 232,144,800.00 921
저장장치 13 205,861,200.00 833
메모리(RAM) 14 200,423,600.00 1354

11. 재귀 CTE로 월 시퀀스를 생성하고, 매출이 없는 월도 포함한 월별 보고서를 만드세요.

2024-01 ~ 2024-12의 12개월 시퀀스를 재귀 CTE로 생성하고, 실제 매출 데이터와 LEFT JOIN합니다.

힌트 1: 재귀 CTE로 '2024-01'부터 시작하여 매월 +1씩 증가하는 시퀀스를 만듭니다. DATE(year_month || '-01', '+1 month')로 다음 달을 계산하고, SUBSTR(..., 1, 7)로 포맷합니다. 종료 조건: year_month < '2024-12'.

정답
WITH RECURSIVE months AS (
    SELECT '2024-01' AS year_month
    UNION ALL
    SELECT SUBSTR(DATE(year_month || '-01', '+1 month'), 1, 7)
    FROM months
    WHERE year_month < '2024-12'
),
actual_revenue AS (
    SELECT
        SUBSTR(ordered_at, 1, 7) AS year_month,
        COUNT(*)                 AS order_count,
        ROUND(SUM(total_amount), 0) AS revenue
    FROM orders
    WHERE ordered_at LIKE '2024%'
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY SUBSTR(ordered_at, 1, 7)
)
SELECT
    m.year_month,
    COALESCE(a.order_count, 0) AS order_count,
    COALESCE(a.revenue, 0)     AS revenue
FROM months AS m
LEFT JOIN actual_revenue AS a ON m.year_month = a.year_month
ORDER BY m.year_month;

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

year_month order_count revenue
2024-01 314 288,908,320.00
2024-02 416 403,127,749.00
2024-03 555 519,844,502.00
2024-04 466 451,877,581.00
2024-05 385 425,264,478.00
2024-06 389 362,715,211.00
2024-07 381 343,929,897.00

12. 3단계 CTE 체인으로 상품 성과 등급을 산출하세요.

CTE 1: 상품별 매출. CTE 2: 매출 기준 NTILE(5) 등급. CTE 3: 등급별 통계. 최종 출력은 등급별 상품 수, 매출 범위, 대표 상품(매출 1위)을 포함합니다.

힌트 1: CTE 3에서 FIRST_VALUE(product_name) OVER (PARTITION BY tier ORDER BY revenue DESC)로 각 등급의 대표 상품을 추출합니다. DISTINCT를 활용하여 등급별 한 행으로 요약합니다.

정답
WITH product_sales AS (
    SELECT
        p.id       AS product_id,
        p.name     AS product_name,
        ROUND(SUM(oi.quantity * oi.unit_price), 0) AS revenue,
        SUM(oi.quantity) AS units_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.ordered_at LIKE '2024%'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY p.id, p.name
),
tiered AS (
    SELECT
        *,
        NTILE(5) OVER (ORDER BY revenue DESC) AS tier
    FROM product_sales
),
tier_summary AS (
    SELECT DISTINCT
        tier,
        COUNT(*) OVER (PARTITION BY tier) AS product_count,
        MIN(revenue) OVER (PARTITION BY tier) AS min_revenue,
        MAX(revenue) OVER (PARTITION BY tier) AS max_revenue,
        ROUND(AVG(revenue) OVER (PARTITION BY tier), 0) AS avg_revenue,
        FIRST_VALUE(product_name) OVER (
            PARTITION BY tier ORDER BY revenue DESC
        ) AS top_product
    FROM tiered
)
SELECT
    tier,
    CASE tier
        WHEN 1 THEN 'S (최상위)'
        WHEN 2 THEN 'A (우수)'
        WHEN 3 THEN 'B (보통)'
        WHEN 4 THEN 'C (저조)'
        WHEN 5 THEN 'D (부진)'
    END AS tier_label,
    product_count,
    min_revenue,
    max_revenue,
    avg_revenue,
    top_product
FROM tier_summary
ORDER BY tier;

실행 결과 (5행)

tier tier_label product_count min_revenue max_revenue avg_revenue top_product
1 S (최상위) 44 39,196,400.00 165,417,800.00 66,665,918.00 Razer Blade 18 블랙
2 A (우수) 44 18,568,800.00 38,656,400.00 26,834,570.00 SteelSeries Arctis Nova Pro Wireless 블랙
3 B (보통) 43 10,200,000.00 18,087,300.00 14,174,188.00 JBL Flip 6 블랙
4 C (저조) 43 5,400,000.00 10,027,500.00 7,904,735.00 Razer DeathAdder V4 Pro 화이트
5 D (부진) 43 159,000.00 5,188,800.00 2,268,381.00 Arctic Liquid Freezer III 240 화이트

13. 재귀 CTE로 연속 날짜 시퀀스를 생성하고, 주문이 없는 날(갭)을 찾으세요.

2024-01-01 ~ 2024-12-31의 연속 날짜를 생성하고, 주문이 0건인 날을 추출합니다.

힌트 1: 재귀 CTE에서 DATE(date_val, '+1 day')로 하루씩 증가합니다. LEFT JOIN으로 실제 주문일과 연결하고, 주문이 NULL인 날을 필터링합니다. 또는 calendar 테이블이 있다면 그것을 활용해도 됩니다.

정답
WITH RECURSIVE date_seq AS (
    SELECT '2024-01-01' AS date_val
    UNION ALL
    SELECT DATE(date_val, '+1 day')
    FROM date_seq
    WHERE date_val < '2024-12-31'
),
daily_orders AS (
    SELECT
        DATE(ordered_at) AS order_date,
        COUNT(*)         AS order_count
    FROM orders
    WHERE ordered_at >= '2024-01-01'
      AND ordered_at < '2025-01-01'
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY DATE(ordered_at)
)
SELECT
    d.date_val AS gap_date,
    CASE CAST(STRFTIME('%w', d.date_val) AS INTEGER)
        WHEN 0 THEN '일'
        WHEN 1 THEN '월'
        WHEN 2 THEN '화'
        WHEN 3 THEN '수'
        WHEN 4 THEN '목'
        WHEN 5 THEN '금'
        WHEN 6 THEN '토'
    END AS day_name
FROM date_seq AS d
LEFT JOIN daily_orders AS o ON d.date_val = o.order_date
WHERE o.order_count IS NULL
ORDER BY d.date_val;

14. 4단계 CTE 체인으로 "코호트 리텐션" 분석을 수행하세요.

CTE 1: 고객별 첫 주문 월(코호트). CTE 2: 고객별 주문 월 목록. CTE 3: 코호트-상대월 매핑. CTE 4: 코호트별 리텐션율.

힌트 1: 상대 월은 (CAST(SUBSTR(order_month,1,4) AS INTEGER)*12 + CAST(SUBSTR(order_month,6,2) AS INTEGER)) - (CAST(SUBSTR(cohort_month,1,4) AS INTEGER)*12 + CAST(SUBSTR(cohort_month,6,2) AS INTEGER))로 계산합니다. 리텐션율 = 해당 월 활성 고객 / 코호트 총 고객 * 100.

정답
WITH cohort 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
),
order_months AS (
    SELECT DISTINCT
        customer_id,
        SUBSTR(ordered_at, 1, 7) AS order_month
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
),
cohort_activity AS (
    SELECT
        co.cohort_month,
        (CAST(SUBSTR(om.order_month, 1, 4) AS INTEGER) * 12
         + CAST(SUBSTR(om.order_month, 6, 2) AS INTEGER))
      - (CAST(SUBSTR(co.cohort_month, 1, 4) AS INTEGER) * 12
         + CAST(SUBSTR(co.cohort_month, 6, 2) AS INTEGER)) AS months_since,
        COUNT(DISTINCT om.customer_id) AS active_customers
    FROM cohort AS co
    INNER JOIN order_months AS om ON co.customer_id = om.customer_id
    GROUP BY co.cohort_month, months_since
),
cohort_size AS (
    SELECT
        cohort_month,
        COUNT(*) AS total_customers
    FROM cohort
    GROUP BY cohort_month
)
SELECT
    ca.cohort_month,
    cs.total_customers,
    ca.months_since,
    ca.active_customers,
    ROUND(100.0 * ca.active_customers / cs.total_customers, 1) AS retention_pct
FROM cohort_activity AS ca
INNER JOIN cohort_size AS cs ON ca.cohort_month = cs.cohort_month
WHERE ca.cohort_month >= '2024-01'
  AND ca.months_since <= 6
ORDER BY ca.cohort_month, ca.months_since;

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

cohort_month total_customers months_since active_customers retention_pct
2024-01 30 0 30 100.00
2024-01 30 1 7 23.30
2024-01 30 2 8 26.70
2024-01 30 3 12 40.00
2024-01 30 4 3 10.00
2024-01 30 5 5 16.70
2024-01 30 6 4 13.30

15. 재귀 CTE로 카테고리 깊이별 통계를 구하고, 트리 형태로 시각화하세요.

각 카테고리의 인덴트(depth에 따른 공백)를 포함하여 트리 구조를 텍스트로 표현하고, 각 카테고리의 직속 상품 수와 하위 전체 상품 수를 함께 표시합니다.

힌트 1: 재귀 CTE로 카테고리 트리를 전개하며, 정렬용 sort_path를 만듭니다(예: '001/003/007'). 인덴트는 REPLACE(SUBSTR(' ', 1, level * 4), ' ', ' ') 또는 CASE level WHEN 0 THEN '' WHEN 1 THEN ' ' WHEN 2 THEN ' ' END 등으로 표현합니다.

정답
WITH RECURSIVE cat_tree AS (
    SELECT
        id,
        parent_id,
        name,
        depth,
        sort_order,
        PRINTF('%03d', sort_order) AS sort_path,
        0 AS level
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    SELECT
        c.id,
        c.parent_id,
        c.name,
        c.depth,
        c.sort_order,
        ct.sort_path || '/' || PRINTF('%03d', c.sort_order),
        ct.level + 1
    FROM categories AS c
    INNER JOIN cat_tree AS ct ON c.parent_id = ct.id
),
direct_products AS (
    SELECT category_id, COUNT(*) AS direct_count
    FROM products
    WHERE is_active = 1
    GROUP BY category_id
)
SELECT
    ct.id,
    CASE ct.level
        WHEN 0 THEN ct.name
        WHEN 1 THEN '  ' || ct.name
        WHEN 2 THEN '    ' || ct.name
    END AS tree_display,
    ct.level,
    COALESCE(dp.direct_count, 0) AS direct_products
FROM cat_tree AS ct
LEFT JOIN direct_products AS dp ON ct.id = dp.category_id
ORDER BY ct.sort_path;

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

id tree_display level direct_products
1 데스크톱 PC 0 0
2 완제품 1 3
3 조립PC 1 9
4 베어본 1 1
5 노트북 0 0
6 일반 노트북 1 8
7 게이밍 노트북 1 6